BriVault

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

Permanent Asset Lock Due to Flawed Payout Logic

Winner Payout Logic Fails to Distribute Full Prize Pool, Resulting in Permanent Asset Lock

Description

The winner payout mechanism, executed in withdraw(), incorrectly calculates the assets to be paid out, resulting in a large portion of the prize pool being permanently locked within the vault contract.

The contract attempts to distribute the entire prize pool (finalizedVaultAsset) based on the winners' proportional shares (totalWinnerShares). Because of either precision loss in Math.mulDiv or, more likely, a failure to dynamically track the remaining pool, the total assets paid out are significantly less than the total prize pool.

In the provided scenario, only 13.13 \text{ ETH} of the total 19.7 \text{ ETH} pool was distributed, leaving 6.56 \text{ ETH} (34% of the pool) permanently locked in the vault, which is irrecoverable by any participant

Risk

Likelihood:

  • Reason 1: The flaw is guaranteed to occur on any market resolution and subsequent withdrawal, as the financial model is broken.

  • Reason 2: The failure is consistent and predictable, resulting in an immediate asset lock after the first winner successfully withdraws.

Impact:

  • Permanent Asset Lock: A large percentage of the total prize pool is permanently inaccessible by participants, representing a total loss of locked value. In the PoC, 6.56 \text{ ETH} remains locked.

    Failure of Core Financial Promise: This bug renders the platform unusable and violates the fundamental principle of a vault using the EIP 4626 standard.

Proof of Concept

The PoC demonstrates the discrepancy: the total assets in the vault before withdrawals (19.7 \text{ ETH}) do not equal the sum of assets paid out, leaving 6.56 \text{ ETH} behind.

Explanation

  1. Initial Pool: 19.7 \text{ ETH}.

  2. Expected Payout: All three winners should receive their net stake back, totaling 19.7 { ETH}, leaving 0 { ETH} in the vault.

  3. Actual Payout: The logs confirm that the three winners combined received only 13.14 { ETH} in total.

  4. Result: The final vault balance is 6.56 { ETH}, confirming that 34% of the pool is permanently locked and inaccessible.

Vault Balance before withdrawals is 19700000000000000000 // 19.7 ETH (Correct)
Vault Balance after withdrawals is 6566666666666666668 // 6.56 ETH (INCORRECT - Locked)
alice Balance after withdrawals is 16566666666666666666 // Payout of 6.56 ETH (INCORRECT)
user1 Balance after withdrawals is 18283333333333333333 // Payout of 3.28 ETH (INCORRECT)
user2 Balance after withdrawals is 18283333333333333333 // Payout of 3.28 ETH (INCORRECT)

Recommended Mitigation

The contract must dynamically track the pool of remaining assets and remaining shares. After each withdrawal, the pool size must be reduced by the amount paid out and the shares burned. This prevents the proportional calculation from using the original static pool size.

Mitigation Code (Conceptual)

This fix requires introducing two new state variables (remainingAssets and remainingShares) that are set equal to finalizedVaultAsset and totalWinnerShares in setWinner() and then dynamically reduced in withdraw().

// BriVault.sol
// New state variables needed:
// uint256 public remainingAssets;
// uint256 public remainingShares;
function withdraw() external winnerSet {
// Requires: remainingAssets and remainingShares state variables set in setWinner().
uint256 shares = balanceOf(msg.sender);
// Corrected Proportional Calculation: Use remaining pool values
uint256 assetToWithdraw = Math.mulDiv(
shares,
- finalizedVaultAsset,
- totalWinnerShares
+ remainingAssets,
+ remainingShares
);
_burn(msg.sender, shares);
// CRITICAL ACCOUNTING UPDATES: Reduce the pool size
+ remainingAssets -= assetToWithdraw;
+ remainingShares -= shares;
IERC20(asset()).safeTransfer(msg.sender, assetToWithdraw);
// ...
}
Updates

Appeal created

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

Dust in the contract

Support

FAQs

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

Give us feedback!