Missing VRF callback failure recovery mechanism leads to permanent protocol deadlock
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.
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 setscurrentDrawRequest.isPending = true - Transaction 2:
randomNumberCallback()may fail due to various reasons, leaving the pending state locked
Vulnerable Scenario:
The following steps demonstrate the deadlock:
- User calls
draw()which successfully executes, settingisPending = trueand requesting VRF randomness - VRF system calls
randomNumberCallback()with the random number - The callback fails due to external transfer failure (e.g., malicious recipient contract reverts)
- Due to transaction atomicity, the callback reverts but the original
draw()transaction remains committed - The contract is now permanently locked with
isPending = true - All future
draw()calls revert withDrawRequestPending()error - No mechanism exists to reset the pending state, causing permanent protocol deadlock
Critical Failure Points in Callback:
// 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
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.
Recommendation
Implement Emergency Reset Function: Add an owner-only function to reset stuck draw requests after a reasonable timeout period:
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);}
Resolution
Golden Grid: Confirmed.
Zealynx: Fixed.

