Skip to content

Commit

Permalink
chore: add fulfilment deadline on GelatoVRFConsumerBase
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonchuah committed Nov 20, 2023
1 parent 430e71a commit 2b1db7b
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 17 deletions.
45 changes: 32 additions & 13 deletions contracts/GelatoVRFConsumerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,32 @@ import {IGelatoVRFConsumer} from "contracts/IGelatoVRFConsumer.sol";
/// for different request IDs by hashing them with the random number provided by drand.
/// For security considerations, refer to the Gelato documentation.
abstract contract GelatoVRFConsumerBase is IGelatoVRFConsumer {
uint256 private constant _PERIOD = 3;
uint256 private constant _GENESIS = 1692803367;
bool[] public requestPending;
mapping(uint256 requestId => bytes32 requestHash) public requestedHash;
mapping(uint256 requestId => uint256 round) public requestDeadline;

/// @notice Returns the address of the dedicated msg.sender.
/// @dev The operator can be found on the Gelato dashboard after a VRF is deployed.
/// @return Address of the operator.
function _operator() internal view virtual returns (address);

/// @notice Returns the number of rounds where randomness is expected to be fulfilled.
/// @dev Rounds of >4 (12 seconds) is recommended to avoid unsuccessful fulfilment.
/// @return uint256 The number of rounds necessary for randomness fulfilment.
function _roundsToFulfill() internal view virtual returns (uint256);

/// @notice User logic to handle the random value received.
/// @param randomness The random number generated by Gelato VRF.
/// @param requestId The ID for the randomness request.
/// @param extraData Additional data from the randomness request.
function _fulfillRandomness(
uint256 randomness,
uint256 requestId,
bytes memory extraData
) internal virtual;

/// @notice Requests randomness from the Gelato VRF.
/// @dev The extraData parameter allows for additional data to be passed to
/// the VRF, which is then forwarded to the callback. This is useful for
Expand All @@ -31,27 +49,17 @@ abstract contract GelatoVRFConsumerBase is IGelatoVRFConsumer {
requestId = uint256(requestPending.length);
requestPending.push();
requestPending[requestId] = true;
requestDeadline[requestId] = _currentRound() + _roundsToFulfill();

bytes memory data = abi.encode(requestId, extraData);
// solhint-disable-next-line not-rely-on-time

bytes memory dataWithTimestamp = abi.encode(data, block.timestamp);
bytes32 requestHash = keccak256(dataWithTimestamp);

requestedHash[requestId] = requestHash;

emit RequestedRandomness(data);
}

/// @notice User logic to handle the random value received.
/// @param randomness The random number generated by Gelato VRF.
/// @param requestId The ID for the randomness request.
/// @param extraData Additional data from the randomness request.
function _fulfillRandomness(
uint256 randomness,
uint256 requestId,
bytes memory extraData
) internal virtual;

/// @notice Callback function used by Gelato VRF to return the random number.
/// The randomness is derived by hashing the provided randomness with the request ID.
/// @param randomness The random number generated by Gelato VRF.
Expand All @@ -71,7 +79,11 @@ abstract contract GelatoVRFConsumerBase is IGelatoVRFConsumer {
bytes32 requestHash = keccak256(dataWithTimestamp);
bool isValidRequestHash = requestHash == requestedHash[requestId];

if (requestPending[requestId] && isValidRequestHash) {
bool isRequestExpired = _currentRound() > requestDeadline[requestId];

if (
requestPending[requestId] && isValidRequestHash && !isRequestExpired
) {
randomness = uint(
keccak256(
abi.encode(
Expand All @@ -87,4 +99,11 @@ abstract contract GelatoVRFConsumerBase is IGelatoVRFConsumer {
requestPending[requestId] = false;
}
}

/// @notice Computes and returns the current round number of drand
function _currentRound() private view returns (uint256 round) {
// solhint-disable-next-line not-rely-on-time
uint256 elapsedFromGenesis = block.timestamp - _GENESIS;
round = (elapsedFromGenesis / _PERIOD) + 1;
}
}
9 changes: 8 additions & 1 deletion contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ contract VRFCoordinatorV2Adapter is

address private immutable _deployer;
address private immutable _operatorAddr;
uint256 private immutable _nrRoundsToFulfill;

mapping(address => bool) public canRequest;

Expand All @@ -48,17 +49,23 @@ contract VRFCoordinatorV2Adapter is
constructor(
address deployer,
address operator,
address[] memory requesters
address[] memory requesters,
uint256 nrRoundsToFulfill
) {
_deployer = deployer;
_operatorAddr = operator;
_updateRequesterPermissions(requesters, true);
_nrRoundsToFulfill = nrRoundsToFulfill;
}

function _operator() internal view override returns (address) {
return _operatorAddr;
}

function _roundsToFulfill() internal view override returns (uint256) {
return _nrRoundsToFulfill;
}

/// @notice Request VRF randomness using Chainlink's VRF Coordinator V2.
/// @param minimumRequestConfirmations Minimum confirmations required for the request.
/// @param numWords Number of random words to generate.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ contract VRFCoordinatorV2AdapterFactory {
/// @return adapter The newly created VRFCoordinatorV2Adapter instance.
function make(
address operator,
address[] memory requesters
address[] memory requesters,
uint256 roundsToFulfill
) external returns (VRFCoordinatorV2Adapter adapter) {
if (adapterRegistry[msg.sender] != address(0))
return VRFCoordinatorV2Adapter(adapterRegistry[msg.sender]);
adapter = new VRFCoordinatorV2Adapter(msg.sender, operator, requesters);
adapter = new VRFCoordinatorV2Adapter(
msg.sender,
operator,
requesters,
roundsToFulfill
);
adapterRegistry[msg.sender] = address(adapter);
emit AdapterCreated(msg.sender, address(adapter));
}
Expand Down
4 changes: 4 additions & 0 deletions contracts/mocks/MockVRFConsumerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ contract MockVRFConsumerBase is GelatoVRFConsumerBase {
return _operatorAddr;
}

function _roundsToFulfill() internal pure override returns (uint256) {
return 4;
}

function requestRandomness(bytes memory data) external returns (uint256) {
return _requestRandomness(data);
}
Expand Down
33 changes: 33 additions & 0 deletions test/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { quicknet } from "../src/drand_info";
import { MockVRFConsumerBase } from "../typechain";
const { deployments, w3f, ethers } = hre;

import { sleep } from "drand-client/util";
import fetch from "node-fetch";
global.fetch = fetch;

Expand Down Expand Up @@ -115,6 +116,38 @@ describe("ConsumerBase Test Suite", function () {
}
});

it("Doesnt store the last round after round elapsed", async () => {
const roundsToFulfill = 2;
const expectedExtraData = "0x12345678";

await mockConsumer.connect(user).requestRandomness(expectedExtraData);
const lastRequestId = await mockConsumer.latestRequestId();
const requestDeadline = await mockConsumer.requestDeadline(lastRequestId);

// catch up to block time (block time is faster than Date.now() in tests)
const blockTimeNow =
(await ethers.provider.getBlock("latest")).timestamp * 1000;
await sleep(blockTimeNow - Date.now());

const exec = await vrf.run({ userArgs });
const res = exec.result as Web3FunctionResultV2;

if (!res.canExec) assert.fail(res.message);

// wait until past deadline
await sleep((roundsToFulfill + 3) * quicknet.period * 1000);

const round = roundAt(Date.now(), quicknet);
expect(round).to.be.gt(requestDeadline);

const callData = res.callData[0];
await dedicatedMsgSender.sendTransaction(callData);

const latestRequestId = await mockConsumer.latestRequestId();

expect(latestRequestId).to.equal(lastRequestId);
});

it("Doesn't execute if no event was emitted", async () => {
const exec = await vrf.run({ userArgs });
const res = exec.result as Web3FunctionResultV2;
Expand Down
57 changes: 56 additions & 1 deletion test/cl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { quicknet } from "../src/drand_info";
import { MockVRFConsumer, VRFCoordinatorV2Adapter } from "../typechain";
const { deployments, w3f, ethers } = hre;

import { sleep } from "drand-client/util";
import fetch from "node-fetch";
global.fetch = fetch;

Expand Down Expand Up @@ -72,9 +73,15 @@ describe("Chainlink Adapter Test Suite", function () {

this.beforeEach(async () => {
const operator = deployer.address;
const roundsToFulfill = 4;
adapter = (await adapterFactory
.connect(deployer)
.deploy(operator, operator, [])) as VRFCoordinatorV2Adapter;
.deploy(
operator,
operator,
[],
roundsToFulfill
)) as VRFCoordinatorV2Adapter;
mockConsumer = (await mockConsumerFactory
.connect(deployer)
.deploy(adapter.address)) as MockVRFConsumer;
Expand Down Expand Up @@ -124,6 +131,54 @@ describe("Chainlink Adapter Test Suite", function () {
}
});

it("Doesnt store the last round after round elapsed", async () => {
// Deploy adapter with roundsToFulfill = 1
const roundsToFulfill = 1;
const operator = deployer.address;
adapter = (await adapterFactory
.connect(deployer)
.deploy(
operator,
operator,
[],
roundsToFulfill
)) as VRFCoordinatorV2Adapter;
mockConsumer = (await mockConsumerFactory
.connect(deployer)
.deploy(adapter.address)) as MockVRFConsumer;
userArgs = { consumerAddress: adapter.address };
await adapter
.connect(deployer)
.updateRequesterPermissions([mockConsumer.address], true);

const numWords = 1;

await mockConsumer.connect(user).requestRandomWords(numWords);
const requestId = await mockConsumer.requestId();
const requestDeadline = await adapter.requestDeadline(requestId);

// catch up to block time (block time is faster than Date.now() in tests)
const blockTimeNow =
(await ethers.provider.getBlock("latest")).timestamp * 1000;
await sleep(blockTimeNow - Date.now());

const exec = await vrf.run({ userArgs });

const res = exec.result as Web3FunctionResultV2;
if (!res.canExec) assert.fail(res.message);

// wait until past deadline
await sleep((roundsToFulfill + 3) * quicknet.period * 1000);

const round = roundAt(Date.now(), quicknet);
expect(round).to.be.gt(requestDeadline);

const calldata = res.callData[0];
await deployer.sendTransaction({ to: calldata.to, data: calldata.data });

await expect(mockConsumer.randomWordsOf(requestId, 0)).to.be.reverted;
});

it("Doesn't execute if no event was emitted", async () => {
const exec = await vrf.run({ userArgs });
const res = exec.result as Web3FunctionResultV2;
Expand Down

0 comments on commit 2b1db7b

Please sign in to comment.