Stratax Contracts

First Flight #57
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: high
Likelihood: high

Finalized Vault Asset Freezing Enables Permanent Fund Lock for Winners

Author Revealed upon completion

Root + Impact

Description

  • Under normal behavior, once the event ends and a winner is finalized, winning participants should be able to withdraw their proportional share of the vault assets without risk of permanent blockage.

The issue arises because the vault permanently snapshots finalizedVaultAsset at winner finalization time, while withdrawals depend on this fixed value even though the actual token balance can later decrease. Any post-finalization token movement causes withdrawals to revert or underpay, permanently locking funds.

// Root cause in the codebase with @> marks to highlight the relevant section
function _setFinallizedVaultBalance () internal returns (uint256) {
// @> finalizedVaultAsset is set once and never updated again
return finalizedVaultAsset = IERC20(asset()).balanceOf(address(this));
}
function withdraw() external winnerSet {
// @> withdrawal trusts finalizedVaultAsset instead of live balance
uint256 assetToWithdraw =
Math.mulDiv(shares, finalizedVaultAsset, totalWinnerShares);
}

Risk

Likelihood:

  • Occurs whenever tokens leave the vault after finalization, including admin recovery, accidental transfers, fee-on-transfer behavior, or token callbacks.

Manifests during normal winner withdrawals following event finalization.

Impact:

  • Winning users are permanently unable to withdraw their funds due to insufficient vault balance.

Funds become irreversibly locked, breaking the core protocol guarantee.

Proof of Concept

  • Withdrawals compute payouts using a frozen snapshot that no longer reflects reality. Once the live balance drops below the snapshot, withdrawals either revert or distribute incorrect amounts, locking remaining funds permanently.

// Step 1: Event ends and winner is finalized
setWinner();
// Step 2: finalizedVaultAsset = 1,000 tokens
// Step 3: Any token transfer reduces vault balance
asset.transfer(address(0xdead), 100);
// Step 4: Winner withdrawal
withdraw(); // reverts or underpays due to balance < finalizedVaultAsset

Recommended Mitigation

  • Use the live vault balance at withdrawal time and cap payouts defensively.

- remove this code
+ add this code
- uint256 vaultAsset = finalizedVaultAsset;
+ uint256 vaultAsset = IERC20(asset()).balanceOf(address(this));
uint256 assetToWithdraw =
Math.mulDiv(shares, vaultAsset, totalWinnerShares);
//Additionally, enforce a post-withdraw invariant:
require(
assetToWithdraw <= IERC20(asset()).balanceOf(address(this)),
"Insufficient vault balance"
);

Support

FAQs

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

Give us feedback!