Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: audit revision 2 #92

Merged
merged 14 commits into from
Nov 24, 2023
Merged
54 changes: 37 additions & 17 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(uint64 requestId => bytes32 requestHash) public requestedHash;
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 @@ -27,31 +45,22 @@ abstract contract GelatoVRFConsumerBase is IGelatoVRFConsumer {
/// @return requestId The ID for the randomness request.
function _requestRandomness(
bytes memory extraData
) internal returns (uint64 requestId) {
requestId = uint64(requestPending.length);
) internal returns (uint256 requestId) {
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,
uint64 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 @@ -63,15 +72,19 @@ abstract contract GelatoVRFConsumerBase is IGelatoVRFConsumer {
require(msg.sender == _operator(), "only operator");

(bytes memory data, ) = abi.decode(dataWithTimestamp, (bytes, uint256));
(uint64 requestId, bytes memory extraData) = abi.decode(
(uint256 requestId, bytes memory extraData) = abi.decode(
data,
(uint64, bytes)
(uint256, bytes)
);

bytes32 requestHash = keccak256(dataWithTimestamp);
bool isValidRequestHash = requestHash == requestedHash[requestId];

if (requestPending[requestId] && isValidRequestHash) {
bool isRequestExpired = _currentRound() > requestDeadline[requestId];
goums marked this conversation as resolved.
Show resolved Hide resolved

if (
requestPending[requestId] && isValidRequestHash && !isRequestExpired
) {
randomness = uint(
keccak256(
abi.encode(
Expand All @@ -87,4 +100,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;
}
}
11 changes: 9 additions & 2 deletions 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 Expand Up @@ -134,7 +141,7 @@ contract VRFCoordinatorV2Adapter is
/// @param data Additional data provided by Gelato VRF, typically containing request details.
function _fulfillRandomness(
uint256 randomness,
uint64 requestId,
uint256 requestId,
bytes memory data
) internal override {
(uint32 numWords, VRFConsumerBaseV2 consumer) = abi.decode(
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
10 changes: 7 additions & 3 deletions contracts/mocks/MockVRFConsumerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {GelatoVRFConsumerBase} from "../GelatoVRFConsumerBase.sol";

contract MockVRFConsumerBase is GelatoVRFConsumerBase {
uint256 public latestRandomness;
uint64 public latestRequestId;
uint256 public latestRequestId;
bytes public latestExtraData;
address private immutable _operatorAddr;

Expand All @@ -17,13 +17,17 @@ contract MockVRFConsumerBase is GelatoVRFConsumerBase {
return _operatorAddr;
}

function requestRandomness(bytes memory data) external returns (uint64) {
function _roundsToFulfill() internal pure override returns (uint256) {
return 4;
}

function requestRandomness(bytes memory data) external returns (uint256) {
return _requestRandomness(data);
}

function _fulfillRandomness(
uint256 randomness,
uint64 requestId,
uint256 requestId,
bytes memory extraData
) internal override {
latestRandomness = randomness;
Expand Down
22 changes: 20 additions & 2 deletions src/drand_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { quicknet } from "./drand_info";

const DRAND_OPTIONS: ChainOptions = {
disableBeaconVerification: false,
noCache: false,
noCache: true,
chainVerificationParams: {
chainHash: quicknet.hash,
publicKey: quicknet.public_key,
Expand Down Expand Up @@ -53,6 +53,22 @@ const clientCache = new HttpChainClientCache([
"https://api.drand.secureweb3.com:6875",
]);

const randomnessOfRoundCache: Map<number, string> = new Map();

async function fetchDrandResponseWithCaching(round: number) {
let randomness = randomnessOfRoundCache.get(round);

if (!randomness) {
const response = await fetchDrandResponse(round);
randomness = response.randomness;
randomnessOfRoundCache.set(response.round, randomness);

return { round: response.round, randomness };
}

return { round, randomness };
}

async function fetchDrandResponse(round: number) {
console.log("Fetching randomness");
const errors = [];
Expand All @@ -73,7 +89,9 @@ export async function getNextRandomness(requestTimeInSec: number) {
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const { round, randomness } = await fetchDrandResponse(nextRound);
const { round, randomness } = await fetchDrandResponseWithCaching(
nextRound
);
console.log(`Fulfilling from round ${round}`);
return randomness;
} catch (e) {
Expand Down
37 changes: 35 additions & 2 deletions test/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ 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;

const DRAND_OPTIONS: ChainOptions = {
disableBeaconVerification: false,
noCache: false,
noCache: true,
chainVerificationParams: {
chainHash: quicknet.hash,
publicKey: quicknet.public_key,
Expand Down Expand Up @@ -99,7 +100,7 @@ describe("ConsumerBase Test Suite", function () {
ethers.BigNumber.from(
ethers.utils.keccak256(
abi.encode(
["uint256", "address", "uint256", "uint64"],
["uint256", "address", "uint256", "uint256"],
[
ethers.BigNumber.from(`0x${randomness}`),
mockConsumer.address,
Expand All @@ -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
61 changes: 58 additions & 3 deletions test/cl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ 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;

const DRAND_OPTIONS: ChainOptions = {
disableBeaconVerification: false,
noCache: false,
noCache: true,
chainVerificationParams: {
chainHash: quicknet.hash,
publicKey: quicknet.public_key,
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 @@ -106,7 +113,7 @@ describe("Chainlink Adapter Test Suite", function () {
const abi = ethers.utils.defaultAbiCoder;
const seed = ethers.utils.keccak256(
abi.encode(
["uint256", "address", "uint256", "uint64"],
["uint256", "address", "uint256", "uint256"],
[
ethers.BigNumber.from(`0x${randomness}`),
adapter.address,
Expand All @@ -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
Loading
Loading