Incorrect Emergency Withdrawal Implementation Allows Extraction of Active Round Funds
emergencyWithdraw() pulls the entire stablecoin balance, which mixes active round funds and unaccounted tokens, breaking prize distribution if invoked.
Description
The emergencyWithdraw() function is designed to recover tokens that are mistakenly sent to the contract. However, its current implementation withdraws the entire stablecoin balance without distinguishing between funds belonging to active rounds and unaccounted tokens.
function emergencyWithdraw() external onlyOwner {uint256 balance = stablecoin.balanceOf(address(this));require(balance > 0, "No balance");_safeTransfer(owner(), balance);}
The contract does not maintain internal accounting to track which funds belong to active lottery rounds. Since the protocol operates with 6 different products running simultaneous rounds, the contract's balance includes:
- Funds from ongoing rounds waiting to reach
maxTickets - Funds from completed rounds waiting for VRF callback
- Any mistakenly sent tokens
The emergency withdrawal function cannot safely fulfill its intended purpose. Attempting to recover mistakenly sent funds will inadvertently extract funds belonging to active lottery rounds, breaking the prize distribution mechanism and causing loss of user funds. This makes the emergency function unusable in its current form.
Recommendation
Implement internal accounting to differentiate between active round funds and unaccounted tokens:
- Add a state variable to track funds in active rounds:
uint256 public totalActiveRoundFunds;
- Update when tickets are purchased:
round.totalCollected += totalPrice;totalActiveRoundFunds += totalPrice;
- Decrease when rounds are distributed:
function _distributeRound(...) private {uint256 total = round.totalCollected;totalActiveRoundFunds -= total;// ... rest of distribution}
- Modify
emergencyWithdraw()to only extract unaccounted funds:
function emergencyWithdraw() external onlyOwner {uint256 balance = stablecoin.balanceOf(address(this));uint256 unaccountedFunds = balance - totalActiveRoundFunds;require(unaccountedFunds > 0, "No unaccounted funds");_safeTransfer(owner(), unaccountedFunds);}
Resolution
Nexalo: Removed emergencyWithdraw() function.
Zealynx: Removing emergencyWithdraw() eliminates the immediate risk but creates a permanent stuck fund problem.

