// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "../interfaces/ILotteryDependencies.sol"; import "./IDirectFundingVrfWrapper.sol"; interface IDirectFundingRoundDeadline { function drawDeadline() external view returns (uint256); } /// @notice Fixed wrapper and round, one paid request, store-only callback, retryable 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. contract DirectFundingVrfAdapter is IRandomnessProvider { struct Config { address wrapper; uint16 confirmations; uint32 callbackGasLimit; uint256 maxRequestFee; } address public immutable round; address public immutable treasury; IDirectFundingVrfWrapper public immutable wrapper; uint16 public immutable confirmations; uint32 public immutable callbackGasLimit; uint256 public immutable maxRequestFee; uint256 public requestId; uint256 public requestFeePaid; uint256 public 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 RequestFeePaid(uint256 indexed requestId, uint256 amount); event Fulfilled(uint256 indexed requestId, uint256 word); 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 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; requestFeePaid = fee; emit Requested(id); emit RequestFeePaid(id, fee); } /// @notice Only the fixed wrapper may store one result; no lottery call occurs in its callback. 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. fulfilled = true; emit Fulfilled(id, words[0]); } /// @notice Any caller may deliver the same stored word. Failure rolls back delivered. function deliver() external nonReentrant { if (!fulfilled || 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 < IDirectFundingRoundDeadline(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); } }