dependencies
{
"dependencies": {
"@chainlink/contracts": "^1.3.0",
"@openzeppelin/contracts": "^5.3.0"
},
"devDependencies": {
"forge-std": "github:foundry-rs/forge-std"
}
}
snippet
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol";
import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
import {RequestStatus, SubscriptionConfig} from "../interfaces/IDataType.sol";
import {ErrorsLib} from "../libraries/ErrorsLib.sol";
import {EventsLib} from "../libraries/EventsLib.sol";
/// @title ChainlinkVRF
/// @notice Wrapper for Chainlink VRF V2
contract ChainlinkVRF is VRFConsumerBaseV2 {
////////////////////////////////////////////////////////////////////////
// State Variables
////////////////////////////////////////////////////////////////////////
/// @notice Chainlink VRF V2 Coordinator
VRFCoordinatorV2Interface internal s_coordinator;
/// @notice Chainlink VRF V2 Subscription Config
SubscriptionConfig internal s_subscriptionConfig;
/// @notice Chainlink Subscription Id
uint64 internal s_subscriptionId;
/// @notice past requests Id
uint256[] internal s_requestIds;
/// @notice `requestId` => `RequestStatus`
mapping(uint256 => RequestStatus) internal s_requestStatus;
////////////////////////////////////////////////////////////////////////
// Constructor
////////////////////////////////////////////////////////////////////////
/// @param _subscriptionId The subscription ID used for funding requests.
/// @param _coordinator The address of the VRF Coordinator contract.
constructor(uint64 _subscriptionId, address _coordinator) VRFConsumerBaseV2(_coordinator) {
s_subscriptionId = _subscriptionId;
s_coordinator = VRFCoordinatorV2Interface(_coordinator);
}
////////////////////////////////////////////////////////////////////////
// Core
////////////////////////////////////////////////////////////////////////
function requestRandomWords() external returns (uint256 requestId) {
requestId = _requestRandomWords();
}
function setSubscriptionConfig(
bytes32 _keyHash,
uint32 _callbackGasLimit,
uint16 _requestConfirmations,
uint32 _numberOfWords
) external {
_setSubscriptionConfig(_keyHash, _callbackGasLimit, _requestConfirmations, _numberOfWords);
}
/// @notice Request for randomness.
/// @return requestId A unique identifier of the request from coordinator.
function _requestRandomWords() internal returns (uint256 requestId) {
// interact: request random words
requestId = s_coordinator.requestRandomWords(
s_subscriptionConfig.keyHash,
s_subscriptionId,
s_subscriptionConfig.requestConfirmations,
s_subscriptionConfig.callbackGasLimit,
s_subscriptionConfig.numberOfWords
);
// effect: record requestId
s_requestIds.push(requestId);
s_requestStatus[requestId] = RequestStatus({randomWords: new uint256[](0), exists: true, fulfilled: false});
emit EventsLib.RequestSent(requestId, s_subscriptionConfig.numberOfWords);
}
/// @notice Set the Subscription Config.
/// @param _keyHash See https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#configurations
/// @param _callbackGasLimit Depends on the number of requested values that you want sent to the fulfillRandomWords() function.
/// Storing each word costs about 20,000 gas.
/// @param _requestConfirmations The minimum confirmations is 3.
/// @param _numberOfWords The number of words for `fulfillRandomWords()`.
function _setSubscriptionConfig(
bytes32 _keyHash,
uint32 _callbackGasLimit,
uint16 _requestConfirmations,
uint32 _numberOfWords
) internal {
// effect: record config
s_subscriptionConfig.keyHash = _keyHash;
s_subscriptionConfig.callbackGasLimit = _callbackGasLimit;
s_subscriptionConfig.requestConfirmations = _requestConfirmations;
s_subscriptionConfig.numberOfWords = _numberOfWords;
emit EventsLib.ConfigSet(_keyHash, _callbackGasLimit, _requestConfirmations, _numberOfWords);
}
////////////////////////////////////////////////////////////////////////
// Override
////////////////////////////////////////////////////////////////////////
/// @notice Handles the VRF response.
/// @param _requestId A unique identifier of the request from coordinator.
/// @param _randomWords The VRF output expanded to the requested number of words.
function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords)
internal
virtual
override(VRFConsumerBaseV2)
{
// check: request is existent
require(s_requestStatus[_requestId].exists, ErrorsLib.RequestNotFound(_requestId));
// effect: record fulfilled data
s_requestStatus[_requestId].fulfilled = true;
s_requestStatus[_requestId].randomWords = _randomWords;
emit EventsLib.RequestFulfilled(_requestId, _randomWords);
}
////////////////////////////////////////////////////////////////////////
// View
////////////////////////////////////////////////////////////////////////
/// @notice Get the Chainlink VRF Coordinator address.
/// @return The Coordinator address.
function getCoordinator() external view returns (address) {
return address(s_coordinator);
}
/// @notice Get the Subscription Id.
/// @return The Subscription Id.
function getSubscriptionId() external view returns (uint64) {
return s_subscriptionId;
}
/// @notice Get the Subscription Config.
/// @return config The Subscription Config.
function getSubscriptionConfig() external view returns (SubscriptionConfig memory config) {
config = s_subscriptionConfig;
}
/// @notice Get the Request Id by index.
/// @param _index The index position in the request IDs array.
/// @return requestId A unique identifier of the request from coordinator.
function getRequestId(uint256 _index) external view returns (uint256 requestId) {
requestId = s_requestIds[_index];
}
/// @notice Get the Request Status by request id.
/// @param _requestId A unique identifier of the request from coordinator.
/// @return status The Request Status.
function getRequestStatus(uint256 _requestId) external view returns (RequestStatus memory status) {
status = s_requestStatus[_requestId];
}
}
testing
deploy and setup VRFCoordinatorV2Mock.sol
for testing
- call
CoordinatorV2#createSubscription()
for new subscription. - call
CoordinatorV2#fundSubscription()
for funding subscription. - deploy consumer contract.
- call
CoordinatorV2#addConsumer()
to add consumer contract to subscription.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
// test utils
import "forge-std/Test.sol";
// test instance
import "../src/interfaces/IDataType.sol";
import "./mocks/ChainlinkVRFMock.sol";
// mock
import "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2Mock.sol";
contract BaseTest is Test {
// mock instance
VRFCoordinatorV2Mock internal coordinatorV2Mock;
// test instance
bytes32 config_keyhash = 0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc;
uint32 config_callbackGasLimit = 100000;
uint16 config_requestConfirmations = 3;
uint32 config_numberOfWords = 2;
uint64 internal subscriptionId;
ChainlinkVRFMock internal vrf_instance;
function setUp() public virtual {
// mock instance
uint96 baseFee = 100000000000000000;
uint96 gasPriceLink = 0;
coordinatorV2Mock = new VRFCoordinatorV2Mock(baseFee, gasPriceLink);
// vrf instance
// - create subscription
subscriptionId = coordinatorV2Mock.createSubscription();
// - fund subscription
coordinatorV2Mock.fundSubscription(subscriptionId, 10000000000000000000000);
// - deploy consumer contract
vrf_instance = new ChainlinkVRFMock(subscriptionId, address(coordinatorV2Mock));
// - add consumer
coordinatorV2Mock.addConsumer(subscriptionId, address(vrf_instance));
// - set config
vrf_instance.setSubscriptionConfig(
config_keyhash, config_callbackGasLimit, config_requestConfirmations, config_numberOfWords
);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "../Base.t.sol";
contract ChainlinkVRFTest is BaseTest {
function test_unit_deployment() external view {
assertEq(vrf_instance.getCoordinator(), address(coordinatorV2Mock));
assertEq(vrf_instance.getSubscriptionId(), subscriptionId);
}
function test_unit_config() external view {
SubscriptionConfig memory config = vrf_instance.getSubscriptionConfig();
assertEq(config.keyHash, config_keyhash);
assertEq(config.callbackGasLimit, config_callbackGasLimit);
assertEq(config.requestConfirmations, config_requestConfirmations);
assertEq(config.numberOfWords, config_numberOfWords);
}
function test_unit_requestRandomWords() external {
RequestStatus memory status;
// request randomness
uint256 requestId = vrf_instance.requestRandomWords();
status = vrf_instance.getRequestStatus(requestId);
assertEq(status.randomWords.length, 0);
assertTrue(status.exists);
assertFalse(status.fulfilled);
// fulfill randomness
coordinatorV2Mock.fulfillRandomWords(requestId, address(vrf_instance));
status = vrf_instance.getRequestStatus(requestId);
assertEq(status.randomWords.length, 2);
assertTrue(status.exists);
assertTrue(status.fulfilled);
}
}