Skip to content

Commit

Permalink
Add vrf coordinator v2.5 mock (#12873)
Browse files Browse the repository at this point in the history
* Add vrf coordinator v2.5 mock

* Fix natspec complaining about inheritdoc references inexistent contract
  • Loading branch information
leeyikjiun authored May 2, 2024
1 parent 8cf34d2 commit 1e50bae
Show file tree
Hide file tree
Showing 3 changed files with 834 additions and 0 deletions.
268 changes: 268 additions & 0 deletions contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// SPDX-License-Identifier: MIT
// A mock for testing code that relies on VRFCoordinatorV2_5.
pragma solidity ^0.8.19;

import {IVRFCoordinatorV2Plus, IVRFSubscriptionV2Plus} from "../dev/interfaces/IVRFCoordinatorV2Plus.sol";
import {VRFV2PlusClient} from "../dev/libraries/VRFV2PlusClient.sol";
import {SubscriptionAPI} from "../dev/SubscriptionAPI.sol";
import {VRFConsumerBaseV2Plus} from "../dev/VRFConsumerBaseV2Plus.sol";

contract VRFCoordinatorV2_5Mock is SubscriptionAPI, IVRFCoordinatorV2Plus {
uint96 public immutable i_base_fee;
uint96 public immutable i_gas_price;
int256 public immutable i_wei_per_unit_link;

error InvalidRequest();
error InvalidRandomWords();
error InvalidExtraArgsTag();
error NotImplemented();

event RandomWordsRequested(
bytes32 indexed keyHash,
uint256 requestId,
uint256 preSeed,
uint256 indexed subId,
uint16 minimumRequestConfirmations,
uint32 callbackGasLimit,
uint32 numWords,
bytes extraArgs,
address indexed sender
);
event RandomWordsFulfilled(
uint256 indexed requestId,
uint256 outputSeed,
uint256 indexed subId,
uint96 payment,
bool nativePayment,
bool success,
bool onlyPremium
);
event ConfigSet();

uint64 internal s_currentSubId;
uint256 internal s_nextRequestId = 1;
uint256 internal s_nextPreSeed = 100;

struct Request {
uint256 subId;
uint32 callbackGasLimit;
uint32 numWords;
bytes extraArgs;
}
mapping(uint256 => Request) internal s_requests; /* requestId */ /* request */

constructor(uint96 _baseFee, uint96 _gasPrice, int256 _weiPerUnitLink) SubscriptionAPI() {
i_base_fee = _baseFee;
i_gas_price = _gasPrice;
i_wei_per_unit_link = _weiPerUnitLink;
setConfig();
}

/**
* @notice Sets the configuration of the vrfv2 mock coordinator
*/
function setConfig() public onlyOwner {
s_config = Config({
minimumRequestConfirmations: 0,
maxGasLimit: 0,
stalenessSeconds: 0,
gasAfterPaymentCalculation: 0,
reentrancyLock: false,
fulfillmentFlatFeeNativePPM: 0,
fulfillmentFlatFeeLinkDiscountPPM: 0,
nativePremiumPercentage: 0,
linkPremiumPercentage: 0
});
emit ConfigSet();
}

function consumerIsAdded(uint256 _subId, address _consumer) public view returns (bool) {
return s_consumers[_consumer][_subId].active;
}

modifier onlyValidConsumer(uint256 _subId, address _consumer) {
if (!consumerIsAdded(_subId, _consumer)) {
revert InvalidConsumer(_subId, _consumer);
}
_;
}

/**
* @notice fulfillRandomWords fulfills the given request, sending the random words to the supplied
* @notice consumer.
*
* @dev This mock uses a simplified formula for calculating payment amount and gas usage, and does
* @dev not account for all edge cases handled in the real VRF coordinator. When making requests
* @dev against the real coordinator a small amount of additional LINK is required.
*
* @param _requestId the request to fulfill
* @param _consumer the VRF randomness consumer to send the result to
*/
function fulfillRandomWords(uint256 _requestId, address _consumer) external nonReentrant {
fulfillRandomWordsWithOverride(_requestId, _consumer, new uint256[](0));
}

/**
* @notice fulfillRandomWordsWithOverride allows the user to pass in their own random words.
*
* @param _requestId the request to fulfill
* @param _consumer the VRF randomness consumer to send the result to
* @param _words user-provided random words
*/
function fulfillRandomWordsWithOverride(uint256 _requestId, address _consumer, uint256[] memory _words) public {
uint256 startGas = gasleft();
if (s_requests[_requestId].subId == 0) {
revert InvalidRequest();
}
Request memory req = s_requests[_requestId];

if (_words.length == 0) {
_words = new uint256[](req.numWords);
for (uint256 i = 0; i < req.numWords; i++) {
_words[i] = uint256(keccak256(abi.encode(_requestId, i)));
}
} else if (_words.length != req.numWords) {
revert InvalidRandomWords();
}

VRFConsumerBaseV2Plus v;
bytes memory callReq = abi.encodeWithSelector(v.rawFulfillRandomWords.selector, _requestId, _words);
s_config.reentrancyLock = true;
// solhint-disable-next-line avoid-low-level-calls, no-unused-vars
(bool success, ) = _consumer.call{gas: req.callbackGasLimit}(callReq);
s_config.reentrancyLock = false;

bool nativePayment = uint8(req.extraArgs[req.extraArgs.length - 1]) == 1;

uint256 rawPayment = i_base_fee + ((startGas - gasleft()) * i_gas_price);
if (!nativePayment) {
rawPayment = (1e18 * rawPayment) / uint256(i_wei_per_unit_link);
}
uint96 payment = uint96(rawPayment);

_chargePayment(payment, nativePayment, req.subId);

delete (s_requests[_requestId]);
emit RandomWordsFulfilled(_requestId, _requestId, req.subId, payment, nativePayment, success, false);
}

function _chargePayment(uint96 payment, bool nativePayment, uint256 subId) internal {
Subscription storage subcription = s_subscriptions[subId];
if (nativePayment) {
uint96 prevBal = subcription.nativeBalance;
if (prevBal < payment) {
revert InsufficientBalance();
}
subcription.nativeBalance = prevBal - payment;
s_withdrawableNative += payment;
} else {
uint96 prevBal = subcription.balance;
if (prevBal < payment) {
revert InsufficientBalance();
}
subcription.balance = prevBal - payment;
s_withdrawableTokens += payment;
}
}

/**
* @notice fundSubscription allows funding a subscription with an arbitrary amount for testing.
*
* @param _subId the subscription to fund
* @param _amount the amount to fund
*/
function fundSubscription(uint256 _subId, uint256 _amount) public {
if (s_subscriptionConfigs[_subId].owner == address(0)) {
revert InvalidSubscription();
}
uint256 oldBalance = s_subscriptions[_subId].balance;
s_subscriptions[_subId].balance += uint96(_amount);
s_totalBalance += uint96(_amount);
emit SubscriptionFunded(_subId, oldBalance, oldBalance + _amount);
}

/// @dev Convert the extra args bytes into a struct
/// @param extraArgs The extra args bytes
/// @return The extra args struct
function _fromBytes(bytes calldata extraArgs) internal pure returns (VRFV2PlusClient.ExtraArgsV1 memory) {
if (extraArgs.length == 0) {
return VRFV2PlusClient.ExtraArgsV1({nativePayment: false});
}
if (bytes4(extraArgs) != VRFV2PlusClient.EXTRA_ARGS_V1_TAG) revert InvalidExtraArgsTag();
return abi.decode(extraArgs[4:], (VRFV2PlusClient.ExtraArgsV1));
}

function requestRandomWords(
VRFV2PlusClient.RandomWordsRequest calldata _req
) external override nonReentrant onlyValidConsumer(_req.subId, msg.sender) returns (uint256) {
uint256 subId = _req.subId;
if (s_subscriptionConfigs[subId].owner == address(0)) {
revert InvalidSubscription();
}

uint256 requestId = s_nextRequestId++;
uint256 preSeed = s_nextPreSeed++;

bytes memory extraArgsBytes = VRFV2PlusClient._argsToBytes(_fromBytes(_req.extraArgs));
s_requests[requestId] = Request({
subId: _req.subId,
callbackGasLimit: _req.callbackGasLimit,
numWords: _req.numWords,
extraArgs: _req.extraArgs
});

emit RandomWordsRequested(
_req.keyHash,
requestId,
preSeed,
_req.subId,
_req.requestConfirmations,
_req.callbackGasLimit,
_req.numWords,
extraArgsBytes,
msg.sender
);
return requestId;
}

/**
* @inheritdoc IVRFSubscriptionV2Plus
*/
function removeConsumer(
uint256 _subId,
address _consumer
) external override onlySubOwner(_subId) onlyValidConsumer(_subId, _consumer) nonReentrant {
if (!s_consumers[_consumer][_subId].active) {
revert InvalidConsumer(_subId, _consumer);
}
address[] memory consumers = s_subscriptionConfigs[_subId].consumers;
uint256 lastConsumerIndex = consumers.length - 1;
for (uint256 i = 0; i < consumers.length; ++i) {
if (consumers[i] == _consumer) {
address last = consumers[lastConsumerIndex];
s_subscriptionConfigs[_subId].consumers[i] = last;
s_subscriptionConfigs[_subId].consumers.pop();
break;
}
}
s_consumers[_consumer][_subId].active = false;
emit SubscriptionConsumerRemoved(_subId, _consumer);
}

/**
* @inheritdoc IVRFSubscriptionV2Plus
*/
function cancelSubscription(uint256 _subId, address _to) external override onlySubOwner(_subId) nonReentrant {
(uint96 balance, uint96 nativeBalance) = _deleteSubscription(_subId);

(bool success, ) = _to.call{value: uint256(nativeBalance)}("");
if (!success) {
revert FailedToSendNative();
}
emit SubscriptionCanceled(_subId, _to, balance, nativeBalance);
}

function pendingRequestExists(uint256 /*subId*/) public pure override returns (bool) {
revert NotImplemented();
}
}
Loading

0 comments on commit 1e50bae

Please sign in to comment.