From ccf4134d48e6b3f6d5d079b2f852b6c96bf6b3be Mon Sep 17 00:00:00 2001 From: "brandon.cs" Date: Wed, 15 Nov 2023 17:16:49 +0800 Subject: [PATCH] chore: audit changes (#88) * [WP-L3]: fix userArgs type * [WP-M1]: restrict addresses that can make requests in VRFCoordinatorV2Adapter * [WP-S2]: get randomness base on log block time * [WP-L4]: fix block range for event filter --- .../VRFCoordinatorV2Adapter.sol | 65 +++++++++++++++++-- .../VRFCoordinatorV2AdapterFactory.sol | 6 +- src/drand_util.ts | 16 +++-- web3-functions/vrf-event-trigger/index.ts | 3 +- web3-functions/vrf/index.ts | 12 ++-- 5 files changed, 83 insertions(+), 19 deletions(-) diff --git a/contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol b/contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol index 93add3c..7df2464 100644 --- a/contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol +++ b/contracts/chainlink_compatible/VRFCoordinatorV2Adapter.sol @@ -14,14 +14,45 @@ contract VRFCoordinatorV2Adapter is VRFCoordinatorV2Stub, GelatoVRFConsumerBase { + /// @dev Emitted when msg.sender is not deployer. + error OnlyDeployer(); + /// @dev Emitted when msg.sender is not authorized requester. + error UnauthorizedRequester(); /// @dev Emitted when an attempt is made to request randomness with zero minimum confirmations. error ZeroConfirmationsRequested(); + /// @notice Event emitted when the permissions for requesters are updated. + /// @param requesters Array of addresses whose permissions for request creation were updated. + /// @param canRequest New permission state; true allows, false disallows request creation. + event RequesterPermissionsUpdated(address[] requesters, bool canRequest); + + address private immutable _deployer; address private immutable _operatorAddr; - /// @dev Constructor to initialize the operator address. - constructor(address operator) { + mapping(address => bool) public canRequest; + + modifier onlyDeployer() { + if (msg.sender != _deployer) { + revert OnlyDeployer(); + } + _; + } + + modifier onlyAuthorizedRequester() { + if (!canRequest[msg.sender]) { + revert UnauthorizedRequester(); + } + _; + } + + constructor( + address deployer, + address operator, + address[] memory requesters + ) { + _deployer = deployer; _operatorAddr = operator; + _updateRequesterPermissions(requesters, true); } function _operator() internal view override returns (address) { @@ -38,7 +69,7 @@ contract VRFCoordinatorV2Adapter is uint16 minimumRequestConfirmations, uint32 /*callbackGasLimit*/, uint32 numWords - ) external override returns (uint256 requestId) { + ) external override onlyAuthorizedRequester returns (uint256 requestId) { return _requestRandomWords( minimumRequestConfirmations, @@ -59,7 +90,7 @@ contract VRFCoordinatorV2Adapter is uint32 /*callbackGasLimit*/, uint32 numWords, VRFConsumerBaseV2 consumer - ) external returns (uint256 requestId) { + ) external onlyAuthorizedRequester returns (uint256 requestId) { return _requestRandomWords( minimumRequestConfirmations, @@ -68,6 +99,16 @@ contract VRFCoordinatorV2Adapter is ); } + /// @notice External function to add or remove addresses that can create requests. + /// @param requesters Array of addresses whose permissions for request creation will be updated. + /// @param newCanRequest True to allow, false to disallow request creation for the addresses. + function updateRequesterPermissions( + address[] memory requesters, + bool newCanRequest + ) external onlyDeployer { + _updateRequesterPermissions(requesters, newCanRequest); + } + /// @notice Internal function to request VRF randomness and emit the request event. /// @dev This function is used to handle randomness requests and emit the appropriate event. /// @param minimumRequestConfirmations Minimum confirmations required for the request. @@ -106,4 +147,20 @@ contract VRFCoordinatorV2Adapter is } consumer.rawFulfillRandomWords(requestId, words); } + + /// @notice Internal function to add or remove addresses that can create requests. + /// @param requesters Array of addresses whose permissions for request creation will be updated. + /// @param newCanRequest True to allow, false to disallow request creation for the addresses. + function _updateRequesterPermissions( + address[] memory requesters, + bool newCanRequest + ) private { + if (requesters.length > 0) { + for (uint256 i; i < requesters.length; i++) { + canRequest[requesters[i]] = newCanRequest; + } + + emit RequesterPermissionsUpdated(requesters, newCanRequest); + } + } } diff --git a/contracts/chainlink_compatible/VRFCoordinatorV2AdapterFactory.sol b/contracts/chainlink_compatible/VRFCoordinatorV2AdapterFactory.sol index c7030d4..9e2d374 100644 --- a/contracts/chainlink_compatible/VRFCoordinatorV2AdapterFactory.sol +++ b/contracts/chainlink_compatible/VRFCoordinatorV2AdapterFactory.sol @@ -17,13 +17,15 @@ contract VRFCoordinatorV2AdapterFactory { /// @notice Create a new instance of VRFCoordinatorV2Adapter. /// @dev Creates a new VRFCoordinatorV2Adapter contract with the provided operator address. /// @param operator The address of the operator that will interact with the adapter. + /// @param requesters The addresses that are able to request for randomness. /// @return adapter The newly created VRFCoordinatorV2Adapter instance. function make( - address operator + address operator, + address[] memory requesters ) external returns (VRFCoordinatorV2Adapter adapter) { if (adapterRegistry[msg.sender] != address(0)) return VRFCoordinatorV2Adapter(adapterRegistry[msg.sender]); - adapter = new VRFCoordinatorV2Adapter(operator); + adapter = new VRFCoordinatorV2Adapter(msg.sender, operator, requesters); adapterRegistry[msg.sender] = address(adapter); emit AdapterCreated(msg.sender, address(adapter)); } diff --git a/src/drand_util.ts b/src/drand_util.ts index 635ea84..f4b9a43 100644 --- a/src/drand_util.ts +++ b/src/drand_util.ts @@ -1,10 +1,10 @@ import shuffle from "lodash/shuffle"; import { - fetchBeacon, - HttpChainClient, - HttpCachingChain, ChainOptions, + HttpCachingChain, + HttpChainClient, + fetchBeacon, roundAt, roundTime, } from "drand-client"; @@ -55,10 +55,12 @@ async function fetchDrandResponse(round?: number) { throw errors.pop(); } -export async function getNextRandomness() { - const now = Date.now(); - const nextRound = roundAt(now, quicknet) + 1; - await sleep(roundTime(quicknet, nextRound) - now); +export async function getNextRandomness(timestampInSec: number) { + const requestTime = timestampInSec * 1000; + const nextRound = roundAt(requestTime, quicknet) + 1; + if (roundTime(quicknet, nextRound) > requestTime) { + await sleep(roundTime(quicknet, nextRound) - requestTime); + } const { round, randomness } = await fetchDrandResponse(nextRound); console.log(`Fulfilling from round ${round}`); return randomness; diff --git a/web3-functions/vrf-event-trigger/index.ts b/web3-functions/vrf-event-trigger/index.ts index c8186d4..aeec765 100644 --- a/web3-functions/vrf-event-trigger/index.ts +++ b/web3-functions/vrf-event-trigger/index.ts @@ -22,7 +22,8 @@ Web3Function.onRun(async (context: Web3FunctionEventContext) => { const consumerAddress = userArgs.consumerAddress as string; const consumer = new Contract(consumerAddress, CONSUMER_ABI, provider); - const randomness = await getNextRandomness(); + const { timestamp } = await provider.getBlock(log.blockHash); + const randomness = await getNextRandomness(timestamp); const encodedRandomness = ethers.BigNumber.from(`0x${randomness}`); const event = consumer.interface.parseLog(log); diff --git a/web3-functions/vrf/index.ts b/web3-functions/vrf/index.ts index 329eb56..2f66071 100644 --- a/web3-functions/vrf/index.ts +++ b/web3-functions/vrf/index.ts @@ -1,5 +1,5 @@ -import * as ethers from "ethers"; import { Log } from "@ethersproject/providers"; +import * as ethers from "ethers"; import { Contract } from "ethers"; import { @@ -26,7 +26,7 @@ Web3Function.onRun(async (context: Web3FunctionContext) => { const provider = multiChainProvider.default(); - const consumerAddress = userArgs.consumerAddress as string[]; + const consumerAddress = userArgs.consumerAddress as string; const consumer = new Contract(consumerAddress, CONSUMER_ABI, provider); const currentBlock = await provider.getBlockNumber(); @@ -45,7 +45,7 @@ Web3Function.onRun(async (context: Web3FunctionContext) => { while (lastBlock < currentBlock && nbRequests < MAX_REQUESTS) { nbRequests++; const fromBlock = lastBlock + 1; - const toBlock = Math.min(fromBlock + MAX_RANGE, currentBlock); + const toBlock = Math.min(lastBlock + MAX_RANGE, currentBlock); try { const eventFilter = { address: consumer.address, @@ -68,11 +68,13 @@ Web3Function.onRun(async (context: Web3FunctionContext) => { // Parse retrieved events console.log(`Matched ${logs.length} new events`); - const randomness = await getNextRandomness(); - const encodedRandomness = ethers.BigNumber.from(`0x${randomness}`); for (const log of logs) { const event = consumer.interface.parseLog(log); const [data] = event.args; + const { timestamp } = await provider.getBlock(log.blockHash); + const randomness = await getNextRandomness(timestamp); + const encodedRandomness = ethers.BigNumber.from(`0x${randomness}`); + callData.push({ to: consumerAddress, data: consumer.interface.encodeFunctionData("fulfillRandomness", [