F-2025-0001·missing-recovery-mechanism

Missing VRF callback failure recovery mechanism leads to permanent protocol deadlock

Fixedlotterypixel-lotterychainlink-vrf
TL;DR

The two-transaction draw process sets isPending = true in draw() but has no recovery if randomNumberCallback() fails, locking all future draws and freezing all user funds permanently.

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

Description

The protocol implements an asynchronous two-transaction draw process where draw() initiates a VRF request and sets isPending = true, while randomNumberCallback() completes the draw and resets the pending state. However, there is no recovery mechanism if the VRF callback transaction fails, creating a permanent deadlock condition.

The vulnerability stems from the fact that these are separate transactions:

  • Transaction 1: draw() successfully executes and permanently sets currentDrawRequest.isPending = true
  • Transaction 2: randomNumberCallback() may fail due to various reasons, leaving the pending state locked

Vulnerable Scenario:

The following steps demonstrate the deadlock:

  1. User calls draw() which successfully executes, setting isPending = true and requesting VRF randomness
  2. VRF system calls randomNumberCallback() with the random number
  3. The callback fails due to external transfer failure (e.g., malicious recipient contract reverts)
  4. Due to transaction atomicity, the callback reverts but the original draw() transaction remains committed
  5. The contract is now permanently locked with isPending = true
  6. All future draw() calls revert with DrawRequestPending() error
  7. No mechanism exists to reset the pending state, causing permanent protocol deadlock

Critical Failure Points in Callback:

solidity
// Any of these external calls can cause total revert:
(bool success, ) = payable(teamContractAddress).call{value: teamAmount}("");
if (!success) revert TransferFailed();
(bool success, ) = payable(foundationAddress).call{value: foundationAmount}("");
if (!success) revert TransferFailed();
(bool success, ) = payable(socialProjectsAddress).call{value: socialProjectsAmount}("");
if (!success) revert TransferFailed();
(bool success, ) = payable(requestCaller).call{value: drawCallerRewardAmount}("");
if (!success) revert TransferFailed();

Additional failure scenarios include:

  • Gas limit exhaustion due to complex callback operations
  • VRF system becoming unresponsive or offline
  • Interface changes in external VRF system
03Section · Impact

Impact

Complete and permanent protocol shutdown with no recovery mechanism. All user funds become permanently locked in the contract as no new draws can be initiated and no existing recovery functions exist. This represents a total loss scenario for all participants.

04Section · Recommendation

Recommendation

Implement Emergency Reset Function: Add an owner-only function to reset stuck draw requests after a reasonable timeout period:

solidity
uint256 public constant EMERGENCY_TIMEOUT = 24 hours;
function emergencyResetDraw() external onlyOwner {
require(currentDrawRequest.isPending, "No pending draw");
require(block.timestamp >= currentDrawTime + EMERGENCY_TIMEOUT, "Timeout not reached");
currentDrawRequest.isPending = false;
currentDrawRequest.requestId = 0;
currentDrawRequest.requestCaller = address(0);
emit EmergencyDrawReset(block.timestamp);
}
05Section · Resolution

Resolution

Golden Grid: Confirmed.

Zealynx: Fixed.

Status
Fixed
F-2025-0001

oog
zealynx

Smart Contract Security Digest

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

© 2026 Zealynx