BriVault

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

cancelParticipation() allows repeated withdrawals before the event starts, enabling fund drain and balance mismatch

Root + Impact

Description

  • Normally, cancelParticipation() should let users withdraw their staked assets once before the event starts, and then clear all related state to prevent further withdrawals.

  • In the current implementation, the function only resets stakedAsset[msg.sender] to 0, but does not clear or guard against multiple calls using the same state conditions. A user can repeatedly call cancelParticipation() before the event starts, receiving multiple refunds and causing the vault balance to deplete.

// Root cause in the codebase with @> marks to highlight the relevant section
function cancelParticipation () public {
if (block.timestamp >= eventStartDate){
revert eventStarted();
}
@> uint256 refundAmount = stakedAsset[msg.sender];
@> stakedAsset[msg.sender] = 0;
uint256 shares = balanceOf(msg.sender);
@> _burn(msg.sender, shares);
@> IERC20(asset()).safeTransfer(msg.sender, refundAmount);
}

Risk

Likelihood:

  • Occurs whenever a user calls cancelParticipation() multiple times before the event start timestamp, since there is no flag or secondary guard preventing repeated execution.

  • The function can also be abused in automated scripts to loop withdrawals before eventStartDate is reached.

Impact:

  • Impact 1: Users can withdraw more tokens than they deposited, draining the vault’s ERC20 balance and preventing correct payouts later.

  • Impact 2: Causes accounting inconsistencies between stakedAsset, totalParticipantShares, and actual vault balance, possibly making the contract insolvent before the event begins.

Proof of Concept

Observed Effect:
Multiple cancelParticipation() calls keep transferring tokens to the same user until vault balance is exhausted, despite stakedAsset[msg.sender] already being cleared.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract PoC_CancelRepeat {
BriVault public vault;
constructor(BriVault _vault) { vault = _vault; }
function exploitCancel() external {
// Assume deposit done previously
vault.cancelParticipation(); // First refund OK
vault.cancelParticipation(); // Second refund – no protection
vault.cancelParticipation(); // Repeated drains vault balance
}
}

Recommended Mitigation

**Explanation: **Adding require(refundAmount > 0) ensures users cannot execute multiple cancellations once their stake is cleared. This prevents double withdrawals and preserves vault integrity before the event.

function cancelParticipation() public {
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
- uint256 refundAmount = stakedAsset[msg.sender];
- stakedAsset[msg.sender] = 0;
-
- uint256 shares = balanceOf(msg.sender);
- _burn(msg.sender, shares);
-
- IERC20(asset()).safeTransfer(msg.sender, refundAmount);
+ uint256 refundAmount = stakedAsset[msg.sender];
+ require(refundAmount > 0, "No deposit to cancel"); // prevents re-entry
+
+ stakedAsset[msg.sender] = 0;
+
+ uint256 shares = balanceOf(msg.sender);
+ require(shares > 0, "No shares found");
+
+ _burn(msg.sender, shares);
+ IERC20(asset()).safeTransfer(msg.sender, refundAmount);
}
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!