// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "../interfaces/ITimeoutRefundRandomnessProvider.sol"; import "./IDirectFundingVrfWrapper.sol"; interface ITimeoutRefundRoundDeadline { function drawDeadline() external view returns (uint256); } /// @notice One paid request, a fixed 24-hour response window, and retryable timely delivery. /// @dev Native donations pay for VRF independently of the USDT pool. No owner or token withdrawal. /// The external wrapper's operator still controls its pricing and service availability. /// Timeout refunds assume the oracle does not selectively withhold results. No independent audit. contract TimeoutRefundVrfAdapter is ITimeoutRefundRandomnessProvider { struct Config { address wrapper; uint16 confirmations; uint32 callbackGasLimit; uint256 maxRequestFee; } uint256 public constant override RESPONSE_WINDOW = 1 days; address public immutable override round; address public immutable treasury; IDirectFundingVrfWrapper public immutable wrapper; uint16 public immutable confirmations; uint32 public immutable callbackGasLimit; uint256 public immutable maxRequestFee; uint256 public override requestId; uint256 public override acceptedAt; uint256 public override seedDeadline; uint256 public override fulfilledAt; uint256 public requestFeePaid; uint256 public override randomWord; bool public fulfilled; bool public delivered; uint256 private lock = 1; error InvalidConfig(); error Unauthorized(); error InvalidState(); error RequestFeeTooHigh(); error InsufficientNativeFunding(); error NativeTransferFailed(); error Reentrancy(); event NativeFunded(address indexed sender, uint256 amount); event Requested(uint256 indexed requestId); event ResponseWindowStarted(uint256 indexed requestId, uint256 acceptedAt, uint256 seedDeadline); event RequestFeePaid(uint256 indexed requestId, uint256 amount); event Fulfilled(uint256 indexed requestId, uint256 word); event FulfillmentRecorded(uint256 indexed requestId, uint256 fulfilledAt, bool timely); event Delivered(uint256 indexed requestId); event NativeRecovered(address indexed treasury, uint256 amount); modifier nonReentrant() { if (lock != 1) revert Reentrancy(); lock = 2; _; lock = 1; } constructor(address round_, address treasury_, Config memory c) { if (round_ == address(0) || treasury_ == address(0) || treasury_ == address(this) || treasury_ == round_ || c.wrapper.code.length == 0 || c.confirmations < 3 || c.confirmations > 200 || c.callbackGasLimit < 100000 || c.callbackGasLimit > 2500000 || c.maxRequestFee == 0) revert InvalidConfig(); round = round_; treasury = treasury_; wrapper = IDirectFundingVrfWrapper(c.wrapper); confirmations = c.confirmations; callbackGasLimit = c.callbackGasLimit; maxRequestFee = c.maxRequestFee; } /// @notice Funding never requests randomness or calls another contract. receive() external payable { emit NativeFunded(msg.sender, msg.value); } function requestRandomness() external override nonReentrant returns (uint256 id) { if (msg.sender != round) revert Unauthorized(); if (requestId != 0) revert InvalidState(); uint256 fee = wrapper.calculateRequestPriceNative(callbackGasLimit, 1); if (fee > maxRequestFee) revert RequestFeeTooHigh(); if (address(this).balance < fee) revert InsufficientNativeFunding(); id = wrapper.requestRandomWordsInNative{value: fee}( callbackGasLimit, confirmations, 1, abi.encodeWithSelector(bytes4(keccak256("VRF ExtraArgsV1")), true) ); // A revert or zero ID also rolls back payment and the wrapper's request. if (id == 0) revert InvalidState(); requestId = id; acceptedAt = block.timestamp; seedDeadline = block.timestamp + RESPONSE_WINDOW; requestFeePaid = fee; emit Requested(id); emit ResponseWindowStarted(id, acceptedAt, seedDeadline); emit RequestFeePaid(id, fee); } /// @notice Only the fixed wrapper may store one result; no lottery call occurs in its callback. /// @dev A valid late callback is stored too, but cannot make a timed-out round drawable. function rawFulfillRandomWords(uint256 id, uint256[] calldata words) external nonReentrant { if (msg.sender != address(wrapper)) revert Unauthorized(); if (requestId == 0 || id != requestId || fulfilled || words.length != 1) revert InvalidState(); randomWord = words[0]; // Zero remains a valid random word. fulfilledAt = block.timestamp; fulfilled = true; emit Fulfilled(id, words[0]); emit FulfillmentRecorded(id, fulfilledAt, fulfilledAt < seedDeadline); } function hasTimelyRandomness() public view override returns (bool) { return fulfilled && fulfilledAt < seedDeadline; } /// @notice Any caller may deliver a timely stored word, even after the response window ends. /// @dev Failure rolls back delivered. Late words can never be delivered to the round. function deliver() external nonReentrant { if (!hasTimelyRandomness() || delivered || round.code.length == 0) revert InvalidState(); delivered = true; IRandomnessReceiver(round).receiveRandomness(requestId, randomWord); emit Delivered(requestId); } /// @notice Any caller may return unused native funds only to the immutable treasury. /// @dev Before a successful request, use the deployed round's actual immutable deadline. /// Recovering after a paid request does not affect the wrapper's prepaid fulfillment. function recoverNative() external nonReentrant { if (requestId == 0) { if (round.code.length == 0 || block.timestamp < ITimeoutRefundRoundDeadline(round).drawDeadline()) { revert InvalidState(); } } uint256 amount = address(this).balance; if (amount == 0) revert InvalidState(); (bool success,) = payable(treasury).call{value: amount}(""); if (!success) revert NativeTransferFailed(); emit NativeRecovered(treasury, amount); } }