/
VRFV2Wrapper.sol
449 lines (404 loc) · 17.6 KB
/
VRFV2Wrapper.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
// SPDX-License-Identifier: MIT
// solhint-disable-next-line one-contract-per-file
pragma solidity ^0.8.6;
import {ConfirmedOwner} from "../shared/access/ConfirmedOwner.sol";
import {TypeAndVersionInterface} from "../interfaces/TypeAndVersionInterface.sol";
import {VRFConsumerBaseV2} from "./VRFConsumerBaseV2.sol";
import {LinkTokenInterface} from "../shared/interfaces/LinkTokenInterface.sol";
import {AggregatorV3Interface} from "../shared/interfaces/AggregatorV3Interface.sol";
import {VRFCoordinatorV2Interface} from "./interfaces/VRFCoordinatorV2Interface.sol";
import {VRFV2WrapperInterface} from "./interfaces/VRFV2WrapperInterface.sol";
import {VRFV2WrapperConsumerBase} from "./VRFV2WrapperConsumerBase.sol";
import {ChainSpecificUtil} from "../ChainSpecificUtil_v0_8_6.sol";
/**
* @notice A wrapper for VRFCoordinatorV2 that provides an interface better suited to one-off
* @notice requests for randomness.
*/
contract VRFV2Wrapper is ConfirmedOwner, TypeAndVersionInterface, VRFConsumerBaseV2, VRFV2WrapperInterface {
event WrapperFulfillmentFailed(uint256 indexed requestId, address indexed consumer);
// solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i
LinkTokenInterface public immutable LINK;
// solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i
AggregatorV3Interface public immutable LINK_ETH_FEED;
// solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i
ExtendedVRFCoordinatorV2Interface public immutable COORDINATOR;
// solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i
uint64 public immutable SUBSCRIPTION_ID;
/// @dev this is the size of a VRF v2 fulfillment's calldata abi-encoded in bytes.
/// @dev proofSize = 13 words = 13 * 256 = 3328 bits
/// @dev commitmentSize = 5 words = 5 * 256 = 1280 bits
/// @dev dataSize = proofSize + commitmentSize = 4608 bits
/// @dev selector = 32 bits
/// @dev total data size = 4608 bits + 32 bits = 4640 bits = 580 bytes
uint32 public s_fulfillmentTxSizeBytes = 580;
// 5k is plenty for an EXTCODESIZE call (2600) + warm CALL (100)
// and some arithmetic operations.
uint256 private constant GAS_FOR_CALL_EXACT_CHECK = 5_000;
// lastRequestId is the request ID of the most recent VRF V2 request made by this wrapper. This
// should only be relied on within the same transaction the request was made.
uint256 public override lastRequestId;
// Configuration fetched from VRFCoordinatorV2
// s_configured tracks whether this contract has been configured. If not configured, randomness
// requests cannot be made.
bool public s_configured;
// s_disabled disables the contract when true. When disabled, new VRF requests cannot be made
// but existing ones can still be fulfilled.
bool public s_disabled;
// s_fallbackWeiPerUnitLink is the backup LINK exchange rate used when the LINK/NATIVE feed is
// stale.
int256 private s_fallbackWeiPerUnitLink;
// s_stalenessSeconds is the number of seconds before we consider the feed price to be stale and
// fallback to fallbackWeiPerUnitLink.
uint32 private s_stalenessSeconds;
// s_fulfillmentFlatFeeLinkPPM is the flat fee in millionths of LINK that VRFCoordinatorV2
// charges.
uint32 private s_fulfillmentFlatFeeLinkPPM;
// Other configuration
// s_wrapperGasOverhead reflects the gas overhead of the wrapper's fulfillRandomWords
// function. The cost for this gas is passed to the user.
uint32 private s_wrapperGasOverhead;
// s_coordinatorGasOverhead reflects the gas overhead of the coordinator's fulfillRandomWords
// function. The cost for this gas is billed to the subscription, and must therefor be included
// in the pricing for wrapped requests. This includes the gas costs of proof verification and
// payment calculation in the coordinator.
uint32 private s_coordinatorGasOverhead;
// s_wrapperPremiumPercentage is the premium ratio in percentage. For example, a value of 0
// indicates no premium. A value of 15 indicates a 15 percent premium.
uint8 private s_wrapperPremiumPercentage;
// s_keyHash is the key hash to use when requesting randomness. Fees are paid based on current gas
// fees, so this should be set to the highest gas lane on the network.
bytes32 internal s_keyHash;
// s_maxNumWords is the max number of words that can be requested in a single wrapped VRF request.
uint8 internal s_maxNumWords;
struct Callback {
address callbackAddress;
uint32 callbackGasLimit;
uint256 requestGasPrice;
int256 requestWeiPerUnitLink;
uint256 juelsPaid;
}
mapping(uint256 => Callback) /* requestID */ /* callback */ public s_callbacks;
constructor(
address _link,
address _linkEthFeed,
address _coordinator
) ConfirmedOwner(msg.sender) VRFConsumerBaseV2(_coordinator) {
LINK = LinkTokenInterface(_link);
LINK_ETH_FEED = AggregatorV3Interface(_linkEthFeed);
COORDINATOR = ExtendedVRFCoordinatorV2Interface(_coordinator);
// Create this wrapper's subscription and add itself as a consumer.
uint64 subId = ExtendedVRFCoordinatorV2Interface(_coordinator).createSubscription();
SUBSCRIPTION_ID = subId;
ExtendedVRFCoordinatorV2Interface(_coordinator).addConsumer(subId, address(this));
}
/**
* @notice setFulfillmentTxSize sets the size of the fulfillment transaction in bytes.
* @param size is the size of the fulfillment transaction in bytes.
*/
function setFulfillmentTxSize(uint32 size) external onlyOwner {
s_fulfillmentTxSizeBytes = size;
}
/**
* @notice setConfig configures VRFV2Wrapper.
*
* @dev Sets wrapper-specific configuration based on the given parameters, and fetches any needed
* @dev VRFCoordinatorV2 configuration from the coordinator.
*
* @param _wrapperGasOverhead reflects the gas overhead of the wrapper's fulfillRandomWords
* function.
*
* @param _coordinatorGasOverhead reflects the gas overhead of the coordinator's
* fulfillRandomWords function.
*
* @param _wrapperPremiumPercentage is the premium ratio in percentage for wrapper requests.
*
* @param _keyHash to use for requesting randomness.
*/
function setConfig(
uint32 _wrapperGasOverhead,
uint32 _coordinatorGasOverhead,
uint8 _wrapperPremiumPercentage,
bytes32 _keyHash,
uint8 _maxNumWords
) external onlyOwner {
s_wrapperGasOverhead = _wrapperGasOverhead;
s_coordinatorGasOverhead = _coordinatorGasOverhead;
s_wrapperPremiumPercentage = _wrapperPremiumPercentage;
s_keyHash = _keyHash;
s_maxNumWords = _maxNumWords;
s_configured = true;
// Get other configuration from coordinator
(, , s_stalenessSeconds, ) = COORDINATOR.getConfig();
s_fallbackWeiPerUnitLink = COORDINATOR.getFallbackWeiPerUnitLink();
(s_fulfillmentFlatFeeLinkPPM, , , , , , , , ) = COORDINATOR.getFeeConfig();
}
/**
* @notice getConfig returns the current VRFV2Wrapper configuration.
*
* @return fallbackWeiPerUnitLink is the backup LINK exchange rate used when the LINK/NATIVE feed
* is stale.
*
* @return stalenessSeconds is the number of seconds before we consider the feed price to be stale
* and fallback to fallbackWeiPerUnitLink.
*
* @return fulfillmentFlatFeeLinkPPM is the flat fee in millionths of LINK that VRFCoordinatorV2
* charges.
*
* @return wrapperGasOverhead reflects the gas overhead of the wrapper's fulfillRandomWords
* function. The cost for this gas is passed to the user.
*
* @return coordinatorGasOverhead reflects the gas overhead of the coordinator's
* fulfillRandomWords function.
*
* @return wrapperPremiumPercentage is the premium ratio in percentage. For example, a value of 0
* indicates no premium. A value of 15 indicates a 15 percent premium.
*
* @return keyHash is the key hash to use when requesting randomness. Fees are paid based on
* current gas fees, so this should be set to the highest gas lane on the network.
*
* @return maxNumWords is the max number of words that can be requested in a single wrapped VRF
* request.
*/
function getConfig()
external
view
returns (
int256 fallbackWeiPerUnitLink,
uint32 stalenessSeconds,
uint32 fulfillmentFlatFeeLinkPPM,
uint32 wrapperGasOverhead,
uint32 coordinatorGasOverhead,
uint8 wrapperPremiumPercentage,
bytes32 keyHash,
uint8 maxNumWords
)
{
return (
s_fallbackWeiPerUnitLink,
s_stalenessSeconds,
s_fulfillmentFlatFeeLinkPPM,
s_wrapperGasOverhead,
s_coordinatorGasOverhead,
s_wrapperPremiumPercentage,
s_keyHash,
s_maxNumWords
);
}
/**
* @notice Calculates the price of a VRF request with the given callbackGasLimit at the current
* @notice block.
*
* @dev This function relies on the transaction gas price which is not automatically set during
* @dev simulation. To estimate the price at a specific gas price, use the estimatePrice function.
*
* @param _callbackGasLimit is the gas limit used to estimate the price.
*/
function calculateRequestPrice(
uint32 _callbackGasLimit
) external view override onlyConfiguredNotDisabled returns (uint256) {
int256 weiPerUnitLink = _getFeedData();
return _calculateRequestPrice(_callbackGasLimit, tx.gasprice, weiPerUnitLink);
}
/**
* @notice Estimates the price of a VRF request with a specific gas limit and gas price.
*
* @dev This is a convenience function that can be called in simulation to better understand
* @dev pricing.
*
* @param _callbackGasLimit is the gas limit used to estimate the price.
* @param _requestGasPriceWei is the gas price in wei used for the estimation.
*/
function estimateRequestPrice(
uint32 _callbackGasLimit,
uint256 _requestGasPriceWei
) external view override onlyConfiguredNotDisabled returns (uint256) {
int256 weiPerUnitLink = _getFeedData();
return _calculateRequestPrice(_callbackGasLimit, _requestGasPriceWei, weiPerUnitLink);
}
function _calculateRequestPrice(
uint256 _gas,
uint256 _requestGasPrice,
int256 _weiPerUnitLink
) internal view returns (uint256) {
// costWei is the base fee denominated in wei (native)
// costWei takes into account the L1 posting costs of the VRF fulfillment
// transaction, if we are on an L2.
uint256 costWei = (_requestGasPrice *
(_gas + s_wrapperGasOverhead + s_coordinatorGasOverhead) +
ChainSpecificUtil._getL1CalldataGasCost(s_fulfillmentTxSizeBytes));
// (1e18 juels/link) * ((wei/gas * (gas)) + l1wei) / (wei/link) == 1e18 juels * wei/link / (wei/link) == 1e18 juels * wei/link * link/wei == juels
// baseFee is the base fee denominated in juels (link)
uint256 baseFee = (1e18 * costWei) / uint256(_weiPerUnitLink);
// feeWithPremium is the fee after the percentage premium is applied
uint256 feeWithPremium = (baseFee * (s_wrapperPremiumPercentage + 100)) / 100;
// feeWithFlatFee is the fee after the flat fee is applied on top of the premium
uint256 feeWithFlatFee = feeWithPremium + (1e12 * uint256(s_fulfillmentFlatFeeLinkPPM));
return feeWithFlatFee;
}
/**
* @notice onTokenTransfer is called by LinkToken upon payment for a VRF request.
*
* @dev Reverts if payment is too low.
*
* @param _sender is the sender of the payment, and the address that will receive a VRF callback
* upon fulfillment.
*
* @param _amount is the amount of LINK paid in Juels.
*
* @param _data is the abi-encoded VRF request parameters: uint32 callbackGasLimit,
* uint16 requestConfirmations, and uint32 numWords.
*/
function onTokenTransfer(address _sender, uint256 _amount, bytes calldata _data) external onlyConfiguredNotDisabled {
// solhint-disable-next-line gas-custom-errors
require(msg.sender == address(LINK), "only callable from LINK");
(uint32 callbackGasLimit, uint16 requestConfirmations, uint32 numWords) = abi.decode(
_data,
(uint32, uint16, uint32)
);
uint32 eip150Overhead = _getEIP150Overhead(callbackGasLimit);
int256 weiPerUnitLink = _getFeedData();
uint256 price = _calculateRequestPrice(callbackGasLimit, tx.gasprice, weiPerUnitLink);
// solhint-disable-next-line gas-custom-errors
require(_amount >= price, "fee too low");
// solhint-disable-next-line gas-custom-errors
require(numWords <= s_maxNumWords, "numWords too high");
uint256 requestId = COORDINATOR.requestRandomWords(
s_keyHash,
SUBSCRIPTION_ID,
requestConfirmations,
callbackGasLimit + eip150Overhead + s_wrapperGasOverhead,
numWords
);
s_callbacks[requestId] = Callback({
callbackAddress: _sender,
callbackGasLimit: callbackGasLimit,
requestGasPrice: tx.gasprice,
requestWeiPerUnitLink: weiPerUnitLink,
juelsPaid: _amount
});
lastRequestId = requestId;
}
/**
* @notice withdraw is used by the VRFV2Wrapper's owner to withdraw LINK revenue.
*
* @param _recipient is the address that should receive the LINK funds.
*
* @param _amount is the amount of LINK in Juels that should be withdrawn.
*/
function withdraw(address _recipient, uint256 _amount) external onlyOwner {
LINK.transfer(_recipient, _amount);
}
/**
* @notice enable this contract so that new requests can be accepted.
*/
function enable() external onlyOwner {
s_disabled = false;
}
/**
* @notice disable this contract so that new requests will be rejected. When disabled, new requests
* @notice will revert but existing requests can still be fulfilled.
*/
function disable() external onlyOwner {
s_disabled = true;
}
// solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore
function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override {
Callback memory callback = s_callbacks[_requestId];
delete s_callbacks[_requestId];
// solhint-disable-next-line gas-custom-errors
require(callback.callbackAddress != address(0), "request not found"); // This should never happen
VRFV2WrapperConsumerBase c;
bytes memory resp = abi.encodeWithSelector(c.rawFulfillRandomWords.selector, _requestId, _randomWords);
bool success = _callWithExactGas(callback.callbackGasLimit, callback.callbackAddress, resp);
if (!success) {
emit WrapperFulfillmentFailed(_requestId, callback.callbackAddress);
}
}
function _getFeedData() private view returns (int256) {
bool staleFallback = s_stalenessSeconds > 0;
uint256 timestamp;
int256 weiPerUnitLink;
(, weiPerUnitLink, , timestamp, ) = LINK_ETH_FEED.latestRoundData();
// solhint-disable-next-line not-rely-on-time
if (staleFallback && s_stalenessSeconds < block.timestamp - timestamp) {
weiPerUnitLink = s_fallbackWeiPerUnitLink;
}
// solhint-disable-next-line gas-custom-errors
require(weiPerUnitLink >= 0, "Invalid LINK wei price");
return weiPerUnitLink;
}
/**
* @dev Calculates extra amount of gas required for running an assembly call() post-EIP150.
*/
function _getEIP150Overhead(uint32 gas) private pure returns (uint32) {
return gas / 63 + 1;
}
/**
* @dev calls target address with exactly gasAmount gas and data as calldata
* or reverts if at least gasAmount gas is not available.
*/
function _callWithExactGas(uint256 gasAmount, address target, bytes memory data) private returns (bool success) {
assembly {
let g := gas()
// Compute g -= GAS_FOR_CALL_EXACT_CHECK and check for underflow
// The gas actually passed to the callee is min(gasAmount, 63//64*gas available).
// We want to ensure that we revert if gasAmount > 63//64*gas available
// as we do not want to provide them with less, however that check itself costs
// gas. GAS_FOR_CALL_EXACT_CHECK ensures we have at least enough gas to be able
// to revert if gasAmount > 63//64*gas available.
if lt(g, GAS_FOR_CALL_EXACT_CHECK) {
revert(0, 0)
}
g := sub(g, GAS_FOR_CALL_EXACT_CHECK)
// if g - g//64 <= gasAmount, revert
// (we subtract g//64 because of EIP-150)
if iszero(gt(sub(g, div(g, 64)), gasAmount)) {
revert(0, 0)
}
// solidity calls check that a contract actually exists at the destination, so we do the same
if iszero(extcodesize(target)) {
revert(0, 0)
}
// call and return whether we succeeded. ignore return data
// call(gas,addr,value,argsOffset,argsLength,retOffset,retLength)
success := call(gasAmount, target, 0, add(data, 0x20), mload(data), 0, 0)
}
return success;
}
function typeAndVersion() external pure virtual override returns (string memory) {
return "VRFV2Wrapper 1.0.0";
}
modifier onlyConfiguredNotDisabled() {
// solhint-disable-next-line gas-custom-errors
require(s_configured, "wrapper is not configured");
// solhint-disable-next-line gas-custom-errors
require(!s_disabled, "wrapper is disabled");
_;
}
}
interface ExtendedVRFCoordinatorV2Interface is VRFCoordinatorV2Interface {
function getConfig()
external
view
returns (
uint16 minimumRequestConfirmations,
uint32 maxGasLimit,
uint32 stalenessSeconds,
uint32 gasAfterPaymentCalculation
);
function getFallbackWeiPerUnitLink() external view returns (int256);
function getFeeConfig()
external
view
returns (
uint32 fulfillmentFlatFeeLinkPPMTier1,
uint32 fulfillmentFlatFeeLinkPPMTier2,
uint32 fulfillmentFlatFeeLinkPPMTier3,
uint32 fulfillmentFlatFeeLinkPPMTier4,
uint32 fulfillmentFlatFeeLinkPPMTier5,
uint24 reqsForTier2,
uint24 reqsForTier3,
uint24 reqsForTier4,
uint24 reqsForTier5
);
}