F-2025-0020·incorrect-emergency-logic

Incorrect Emergency Withdrawal Implementation Allows Extraction of Active Round Funds

Acknowledgedrafflelotteryvrf
TL;DR

emergencyWithdraw() pulls the entire stablecoin balance, which mixes active round funds and unaccounted tokens, breaking prize distribution if invoked.

Severity
LOW
Impact
HIGH
Likelihood
LOW
Method
MManual review
CAT.
Complexity
HIGH
Exploitability
LOW
02Section · Description

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.

solidity
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.

03Section · Recommendation

Recommendation

Implement internal accounting to differentiate between active round funds and unaccounted tokens:

  1. Add a state variable to track funds in active rounds:
solidity
uint256 public totalActiveRoundFunds;
  1. Update when tickets are purchased:
solidity
round.totalCollected += totalPrice;
totalActiveRoundFunds += totalPrice;
  1. Decrease when rounds are distributed:
solidity
function _distributeRound(...) private {
uint256 total = round.totalCollected;
totalActiveRoundFunds -= total;
// ... rest of distribution
}
  1. Modify emergencyWithdraw() to only extract unaccounted funds:
solidity
function emergencyWithdraw() external onlyOwner {
uint256 balance = stablecoin.balanceOf(address(this));
uint256 unaccountedFunds = balance - totalActiveRoundFunds;
require(unaccountedFunds > 0, "No unaccounted funds");
_safeTransfer(owner(), unaccountedFunds);
}
04Section · Resolution

Resolution

Nexalo: Removed emergencyWithdraw() function.

Zealynx: Removing emergencyWithdraw() eliminates the immediate risk but creates a permanent stuck fund problem.

F-2025-0020

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx