From 2b1db7b2ab553a9bb7d77d383dd7ade615febc04 Mon Sep 17 00:00:00 2001 From: Brandon Chuah Date: Mon, 20 Nov 2023 16:21:03 +0800 Subject: [PATCH] chore: add fulfilment deadline on GelatoVRFConsumerBase --- contracts/GelatoVRFConsumerBase.sol | 45 ++++++++++----- .../VRFCoordinatorV2Adapter.sol | 9 ++- .../VRFCoordinatorV2AdapterFactory.sol | 10 +++- contracts/mocks/MockVRFConsumerBase.sol | 4 ++ test/base.test.ts | 33 +++++++++++ test/cl.test.ts | 57 ++++++++++++++++++- 6 files changed, 141 insertions(+), 17 deletions(-) diff --git a/contracts/GelatoVRFConsumerBase.sol b/contracts/GelatoVRFConsumerBase.sol index 7dc752b..743b05b 100644 --- a/contracts/GelatoVRFConsumerBase.sol +++ b/contracts/GelatoVRFConsumerBase.sol @@ -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 @@ -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. @@ -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( @@ -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; + } } diff --git a/contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol b/contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol index 0a86667..0a6aedf 100644 --- a/contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol +++ b/contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol @@ -28,6 +28,7 @@ contract VRFCoordinatorV2Adapter is address private immutable _deployer; address private immutable _operatorAddr; + uint256 private immutable _nrRoundsToFulfill; mapping(address => bool) public canRequest; @@ -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. diff --git a/contracts/chainlink_compatible/VRFCoordinatorV2AdapterFactory.sol b/contracts/chainlink_compatible/VRFCoordinatorV2AdapterFactory.sol index 9e2d374..5c51e3e 100644 --- a/contracts/chainlink_compatible/VRFCoordinatorV2AdapterFactory.sol +++ b/contracts/chainlink_compatible/VRFCoordinatorV2AdapterFactory.sol @@ -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)); } diff --git a/contracts/mocks/MockVRFConsumerBase.sol b/contracts/mocks/MockVRFConsumerBase.sol index 3777b53..351aeea 100644 --- a/contracts/mocks/MockVRFConsumerBase.sol +++ b/contracts/mocks/MockVRFConsumerBase.sol @@ -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); } diff --git a/test/base.test.ts b/test/base.test.ts index b0dca30..5426a93 100644 --- a/test/base.test.ts +++ b/test/base.test.ts @@ -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; @@ -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; diff --git a/test/cl.test.ts b/test/cl.test.ts index 7c8b04d..6d75712 100644 --- a/test/cl.test.ts +++ b/test/cl.test.ts @@ -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; @@ -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; @@ -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;