// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "../interfaces/ILotteryDependencies.sol"; import "./IVrfV25.sol"; /// @notice One fixed round and coordinator; one request, one stored result, retryable delivery. /// @dev The subscription is owned by this contract. No cancel/withdraw/consumer/owner/coordinator /// setter is exposed. Excess oracle funding is not recoverable through this adapter. contract ImmutableVrfAdapter is IRandomnessProvider { struct Config { address coordinator; bytes32 keyHash; uint16 confirmations; uint32 callbackGasLimit; bool nativePayment; } address public immutable round; IVrfV25 public immutable coordinator; bytes32 public immutable keyHash; uint16 public immutable confirmations; uint32 public immutable callbackGasLimit; bool public immutable nativePayment; uint256 public immutable subscriptionId; uint256 public requestId; uint256 public randomWord; bool public fulfilled; bool public delivered; uint256 private lock = 1; error InvalidConfig(); error Unauthorized(); error InvalidState(); error Reentrancy(); event Requested(uint256 indexed requestId); event Fulfilled(uint256 indexed requestId, uint256 word); event Delivered(uint256 indexed requestId); modifier nonReentrant() { if (lock != 1) revert Reentrancy(); lock = 2; _; lock = 1; } constructor(address round_, Config memory c) { if (round_ == address(0) || c.coordinator.code.length == 0 || c.keyHash == bytes32(0) || c.confirmations < 3 || c.confirmations > 200 || c.callbackGasLimit < 100000 || c.callbackGasLimit > 2500000) revert InvalidConfig(); round = round_; coordinator = IVrfV25(c.coordinator); keyHash = c.keyHash; confirmations = c.confirmations; callbackGasLimit = c.callbackGasLimit; nativePayment = c.nativePayment; uint256 id = IVrfV25(c.coordinator).createSubscription(); if (id == 0) revert InvalidConfig(); IVrfV25(c.coordinator).addConsumer(id, address(this)); subscriptionId = id; } function requestRandomness() external nonReentrant returns (uint256 id) { if (msg.sender != round) revert Unauthorized(); if (requestId != 0) revert InvalidState(); id = coordinator.requestRandomWords(IVrfV25.RandomWordsRequest({ keyHash: keyHash, subId: subscriptionId, requestConfirmations: confirmations, callbackGasLimit: callbackGasLimit, numWords: 1, extraArgs: abi.encodeWithSelector(bytes4(keccak256("VRF ExtraArgsV1")), nativePayment) })); if (id == 0) revert InvalidState(); requestId = id; emit Requested(id); } /// @notice Coordinator-only callback stores a result and makes no external call. function rawFulfillRandomWords(uint256 id, uint256[] calldata words) external nonReentrant { if (msg.sender != address(coordinator)) revert Unauthorized(); if (requestId == 0 || id != requestId || fulfilled || words.length != 1) revert InvalidState(); randomWord = words[0]; // Zero is a valid random value. fulfilled = true; emit Fulfilled(id, words[0]); } /// @notice Anyone may deliver the same stored result. A failed call 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); } }