From a1ca3fff06a5c7016da86ad4866bf687b3f067b9 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Fri, 16 Jun 2023 19:19:04 +0100 Subject: [PATCH 01/30] feat(contracts): vrf consumer + chainlink base --- contracts/src/rng/VRFConsumerBaseV2.sol | 133 +++++++++++ contracts/src/rng/VRFConsumerV2.sol | 225 ++++++++++++++++++ .../interfaces/VRFCoordinatorV2Interface.sol | 112 +++++++++ 3 files changed, 470 insertions(+) create mode 100644 contracts/src/rng/VRFConsumerBaseV2.sol create mode 100644 contracts/src/rng/VRFConsumerV2.sol create mode 100644 contracts/src/rng/interfaces/VRFCoordinatorV2Interface.sol diff --git a/contracts/src/rng/VRFConsumerBaseV2.sol b/contracts/src/rng/VRFConsumerBaseV2.sol new file mode 100644 index 000000000..c2974c13e --- /dev/null +++ b/contracts/src/rng/VRFConsumerBaseV2.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/** **************************************************************************** + * @notice Interface for contracts using VRF randomness + * ***************************************************************************** + * @dev PURPOSE + * + * @dev Reggie the Random Oracle (not his real job) wants to provide randomness + * @dev to Vera the verifier in such a way that Vera can be sure he's not + * @dev making his output up to suit himself. Reggie provides Vera a public key + * @dev to which he knows the secret key. Each time Vera provides a seed to + * @dev Reggie, he gives back a value which is computed completely + * @dev deterministically from the seed and the secret key. + * + * @dev Reggie provides a proof by which Vera can verify that the output was + * @dev correctly computed once Reggie tells it to her, but without that proof, + * @dev the output is indistinguishable to her from a uniform random sample + * @dev from the output space. + * + * @dev The purpose of this contract is to make it easy for unrelated contracts + * @dev to talk to Vera the verifier about the work Reggie is doing, to provide + * @dev simple access to a verifiable source of randomness. It ensures 2 things: + * @dev 1. The fulfillment came from the VRFCoordinator + * @dev 2. The consumer contract implements fulfillRandomWords. + * ***************************************************************************** + * @dev USAGE + * + * @dev Calling contracts must inherit from VRFConsumerBase, and can + * @dev initialize VRFConsumerBase's attributes in their constructor as + * @dev shown: + * + * @dev contract VRFConsumer { + * @dev constructor(, address _vrfCoordinator, address _link) + * @dev VRFConsumerBase(_vrfCoordinator) public { + * @dev + * @dev } + * @dev } + * + * @dev The oracle will have given you an ID for the VRF keypair they have + * @dev committed to (let's call it keyHash). Create subscription, fund it + * @dev and your consumer contract as a consumer of it (see VRFCoordinatorInterface + * @dev subscription management functions). + * @dev Call requestRandomWords(keyHash, subId, minimumRequestConfirmations, + * @dev callbackGasLimit, numWords), + * @dev see (VRFCoordinatorInterface for a description of the arguments). + * + * @dev Once the VRFCoordinator has received and validated the oracle's response + * @dev to your request, it will call your contract's fulfillRandomWords method. + * + * @dev The randomness argument to fulfillRandomWords is a set of random words + * @dev generated from your requestId and the blockHash of the request. + * + * @dev If your contract could have concurrent requests open, you can use the + * @dev requestId returned from requestRandomWords to track which response is associated + * @dev with which randomness request. + * @dev See "SECURITY CONSIDERATIONS" for principles to keep in mind, + * @dev if your contract could have multiple requests in flight simultaneously. + * + * @dev Colliding `requestId`s are cryptographically impossible as long as seeds + * @dev differ. + * + * ***************************************************************************** + * @dev SECURITY CONSIDERATIONS + * + * @dev A method with the ability to call your fulfillRandomness method directly + * @dev could spoof a VRF response with any random value, so it's critical that + * @dev it cannot be directly called by anything other than this base contract + * @dev (specifically, by the VRFConsumerBase.rawFulfillRandomness method). + * + * @dev For your users to trust that your contract's random behavior is free + * @dev from malicious interference, it's best if you can write it so that all + * @dev behaviors implied by a VRF response are executed *during* your + * @dev fulfillRandomness method. If your contract must store the response (or + * @dev anything derived from it) and use it later, you must ensure that any + * @dev user-significant behavior which depends on that stored value cannot be + * @dev manipulated by a subsequent VRF request. + * + * @dev Similarly, both miners and the VRF oracle itself have some influence + * @dev over the order in which VRF responses appear on the blockchain, so if + * @dev your contract could have multiple VRF requests in flight simultaneously, + * @dev you must ensure that the order in which the VRF responses arrive cannot + * @dev be used to manipulate your contract's user-significant behavior. + * + * @dev Since the block hash of the block which contains the requestRandomness + * @dev call is mixed into the input to the VRF *last*, a sufficiently powerful + * @dev miner could, in principle, fork the blockchain to evict the block + * @dev containing the request, forcing the request to be included in a + * @dev different block with a different hash, and therefore a different input + * @dev to the VRF. However, such an attack would incur a substantial economic + * @dev cost. This cost scales with the number of blocks the VRF oracle waits + * @dev until it calls responds to a request. It is for this reason that + * @dev that you can signal to an oracle you'd like them to wait longer before + * @dev responding to the request (however this is not enforced in the contract + * @dev and so remains effective only in the case of unmodified oracle software). + */ +abstract contract VRFConsumerBaseV2 { + error OnlyCoordinatorCanFulfill(address have, address want); + address private immutable vrfCoordinator; + + /** + * @param _vrfCoordinator address of VRFCoordinator contract + */ + constructor(address _vrfCoordinator) { + vrfCoordinator = _vrfCoordinator; + } + + /** + * @notice fulfillRandomness handles the VRF response. Your contract must + * @notice implement it. See "SECURITY CONSIDERATIONS" above for important + * @notice principles to keep in mind when implementing your fulfillRandomness + * @notice method. + * + * @dev VRFConsumerBaseV2 expects its subcontracts to have a method with this + * @dev signature, and will call it once it has verified the proof + * @dev associated with the randomness. (It is triggered via a call to + * @dev rawFulfillRandomness, below.) + * + * @param requestId The Id initially returned by requestRandomness + * @param randomWords the VRF output expanded to the requested number of words + */ + function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal virtual; + + // rawFulfillRandomness is called by VRFCoordinator when it receives a valid VRF + // proof. rawFulfillRandomness then calls fulfillRandomness, after validating + // the origin of the call + function rawFulfillRandomWords(uint256 requestId, uint256[] memory randomWords) external { + if (msg.sender != vrfCoordinator) { + revert OnlyCoordinatorCanFulfill(msg.sender, vrfCoordinator); + } + fulfillRandomWords(requestId, randomWords); + } +} diff --git a/contracts/src/rng/VRFConsumerV2.sol b/contracts/src/rng/VRFConsumerV2.sol new file mode 100644 index 000000000..108958fa9 --- /dev/null +++ b/contracts/src/rng/VRFConsumerV2.sol @@ -0,0 +1,225 @@ +//SPDX-License-Identifier: MIT + +/** + * @authors: [@malatrax] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ +pragma solidity 0.8.18; + +import "./RNG.sol"; +import "./VRFConsumerBaseV2.sol"; +import "./interfaces/VRFCoordinatorV2Interface.sol"; + +// Interface to call passPhase in the callback function +interface ISortitionModule { + function passPhase() external; +} + +/** + * @title Random Number Generator using Chainlink Verifiable Resolution Mechanism v2 on Arbitrum - Subscription Method - Consumer + * @author Simon Malatrait + * @dev This contract implements the RNG standard and inherits from VRFConsumerBaseV2 to use Chainlink Verifiable Randomness Mechanism. + * @dev It allows to store the random number associated to the requests made. + * @dev Chainlink Subscription Method Documentation: https://docs.chain.link/vrf/v2/subscription + * @dev Chainlink Subscription Method Network Parameters: https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet + * @dev For SECURITY CONSIDERATIONS, you might also have look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol + * @dev For SECURITY CONSIDERATIONS, you might also have look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol + */ +contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { + // ************************************* // + // * Events * // + // ************************************* // + + /** + * @dev Emitted when a request is sent to the VRF Coordinator + * @param requestId The ID of the request + * @param numWords The number of random values requested + */ + event RequestSent(uint256 indexed requestId, uint32 numWords); + + /** + * Emitted when a request has been fulfilled. + * @param requestId The ID of the request + * @param randomWords The random values answering the request. + */ + event RequestFulfilled(uint256 indexed requestId, uint256[] randomWords); + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; + bytes32 public keyHash; + VRFCoordinatorV2Interface vrfCoordinator; + uint64 public subscriptionId; + uint32 public callbackGasLimit; + ISortitionModule sortitionModule; + uint32 public numWords; + uint16 public requestConfirmations; + uint256 public lastRequestId; + + mapping(uint256 => uint256) public requestsToRandomWords; // s_requests[requestId] = randomWord + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyBySortitionModule() { + require(msg.sender == address(sortitionModule), "Access not allowed: SortitionModule only"); + _; + } + + modifier onlyByGovernor() { + require(msg.sender == governor, "Access not allowed: Governor only"); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + /** + * @dev Constructs the ChainlinkRNG contract. + * @param _governor The Governor of the contract. + * @param _vrfCoordinator The address of the VRFCoordinator contract. + * @param _sortitionModule The address of the SortitionModule contract. + * @param _keyHash The gas lane key hash value - Defines the maximum gas price you are willing to pay for a request in wei (ID of the off-chain VRF job). + * @param _subscriptionId The unique identifier of the subscription used for funding requests. + * @param _requestConfirmations How many confirmations the Chainlink node should wait before responding. + * @param _callbackGasLimit The limit for how much gas to use for the callback request to the contract's fulfillRandomWords() function. + * @param _numWords How many random values to request. + * @dev https://docs.chain.link/vrf/v2/subscription/examples/get-a-random-number#analyzing-the-contract + */ + constructor( + address _governor, + address _vrfCoordinator, + address _sortitionModule, + bytes32 _keyHash, + uint64 _subscriptionId, + uint16 _requestConfirmations, + uint32 _callbackGasLimit, + uint32 _numWords + ) VRFConsumerBaseV2(_vrfCoordinator) { + governor = _governor; + vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); + sortitionModule = ISortitionModule(_sortitionModule); + keyHash = _keyHash; + subscriptionId = _subscriptionId; + requestConfirmations = _requestConfirmations; + callbackGasLimit = _callbackGasLimit; + numWords = _numWords; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /** + * @dev Changes the `vrfCoordinator` storage variable. + * @param _vrfCoordinator The new value for the `vrfCoordinator` storage variable. + */ + function changeVrfCoordinator(address _vrfCoordinator) external onlyByGovernor { + vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); + } + + /** + * @dev Changes the `sortitionModule` storage variable. + * @param _sortitionModule The new value for the `sortitionModule` storage variable. + */ + function changeSortitionModule(address _sortitionModule) external onlyByGovernor { + sortitionModule = ISortitionModule(_sortitionModule); + } + + /** + * @dev Changes the `keyHash` storage variable. + * @param _keyHash The new value for the `keyHash` storage variable. + */ + function changeKeyHash(bytes32 _keyHash) external onlyByGovernor { + keyHash = _keyHash; + } + + /** + * @dev Changes the `subscriptionId` storage variable. + * @param _subscriptionId The new value for the `subscriptionId` storage variable. + */ + function changeSubscriptionId(uint64 _subscriptionId) external onlyByGovernor { + subscriptionId = _subscriptionId; + } + + /** + * @dev Changes the `requestConfirmations` storage variable. + * @param _requestConfirmations The new value for the `requestConfirmations` storage variable. + */ + function changeRequestConfirmations(uint16 _requestConfirmations) external onlyByGovernor { + requestConfirmations = _requestConfirmations; + } + + /** + * @dev Changes the `callbackGasLimit` storage variable. + * @param _callbackGasLimit The new value for the `callbackGasLimit` storage variable. + */ + function changeCallbackGasLimit(uint32 _callbackGasLimit) external onlyByGovernor { + callbackGasLimit = _callbackGasLimit; + } + + /** + * @dev Changes the `numWords` storage variable. + * @param _numWords The new value for the `numWords` storage variable. + */ + function changeNumWord(uint32 _numWords) external onlyByGovernor { + numWords = _numWords; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /** + * @dev Submit a request to the VRF Coordinator contract with the specified parameters. + * @dev Assumes the subscription is funded sufficiently; "Words" refers to unit of data in Computer Science + * Note Buffer of one requestId, as in RandomizerRNG, which should be enough with the callback function. + */ + function requestRandomness(uint256 /* _block */) external onlyBySortitionModule { + // Will revert if subscription is not set and funded. + uint256 requestId = vrfCoordinator.requestRandomWords( + keyHash, + subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords + ); + lastRequestId = requestId; + emit RequestSent(requestId, numWords); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /** + * @dev Callback function used by VRF Coordinator + * @dev Stores the random number given by the VRF Coordinator and calls passPhase function on SortitionModule. + * @param _requestId The same request Id initially returned by `vrfCoordinator.requestRandomWords` and stored in the `lastRequestId` storage variable. + * @param _randomWords - array of random results from VRF Coordinator + */ + function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { + requestsToRandomWords[_requestId] = _randomWords[0]; + emit RequestFulfilled(_requestId, _randomWords); + sortitionModule.passPhase(); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * @dev Get the random value associated to `lastRequestId` + * @return randomNumber The random number. If the value is not ready or has not been required it returns 0. + */ + function receiveRandomness(uint256 /* _block */) external view returns (uint256 randomNumber) { + randomNumber = requestsToRandomWords[lastRequestId]; + } +} diff --git a/contracts/src/rng/interfaces/VRFCoordinatorV2Interface.sol b/contracts/src/rng/interfaces/VRFCoordinatorV2Interface.sol new file mode 100644 index 000000000..4143ceece --- /dev/null +++ b/contracts/src/rng/interfaces/VRFCoordinatorV2Interface.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface VRFCoordinatorV2Interface { + /** + * @notice Get configuration relevant for making requests + * @return minimumRequestConfirmations global min for request confirmations + * @return maxGasLimit global max for request gas limit + * @return s_provingKeyHashes list of registered key hashes + */ + function getRequestConfig() external view returns (uint16, uint32, bytes32[] memory); + + /** + * @notice Request a set of random words. + * @param keyHash - Corresponds to a particular oracle job which uses + * that key for generating the VRF proof. Different keyHash's have different gas price + * ceilings, so you can select a specific one to bound your maximum per request cost. + * @param subId - The ID of the VRF subscription. Must be funded + * with the minimum subscription balance required for the selected keyHash. + * @param minimumRequestConfirmations - How many blocks you'd like the + * oracle to wait before responding to the request. See SECURITY CONSIDERATIONS + * for why you may want to request more. The acceptable range is + * [minimumRequestBlockConfirmations, 200]. + * @param callbackGasLimit - How much gas you'd like to receive in your + * fulfillRandomWords callback. Note that gasleft() inside fulfillRandomWords + * may be slightly less than this amount because of gas used calling the function + * (argument decoding etc.), so you may need to request slightly more than you expect + * to have inside fulfillRandomWords. The acceptable range is + * [0, maxGasLimit] + * @param numWords - The number of uint256 random values you'd like to receive + * in your fulfillRandomWords callback. Note these numbers are expanded in a + * secure way by the VRFCoordinator from a single random value supplied by the oracle. + * @return requestId - A unique identifier of the request. Can be used to match + * a request to a response in fulfillRandomWords. + */ + function requestRandomWords( + bytes32 keyHash, + uint64 subId, + uint16 minimumRequestConfirmations, + uint32 callbackGasLimit, + uint32 numWords + ) external returns (uint256 requestId); + + /** + * @notice Create a VRF subscription. + * @return subId - A unique subscription id. + * @dev You can manage the consumer set dynamically with addConsumer/removeConsumer. + * @dev Note to fund the subscription, use transferAndCall. For example + * @dev LINKTOKEN.transferAndCall( + * @dev address(COORDINATOR), + * @dev amount, + * @dev abi.encode(subId)); + */ + function createSubscription() external returns (uint64 subId); + + /** + * @notice Get a VRF subscription. + * @param subId - ID of the subscription + * @return balance - LINK balance of the subscription in juels. + * @return reqCount - number of requests for this subscription, determines fee tier. + * @return owner - owner of the subscription. + * @return consumers - list of consumer address which are able to use this subscription. + */ + function getSubscription( + uint64 subId + ) external view returns (uint96 balance, uint64 reqCount, address owner, address[] memory consumers); + + /** + * @notice Request subscription owner transfer. + * @param subId - ID of the subscription + * @param newOwner - proposed new owner of the subscription + */ + function requestSubscriptionOwnerTransfer(uint64 subId, address newOwner) external; + + /** + * @notice Request subscription owner transfer. + * @param subId - ID of the subscription + * @dev will revert if original owner of subId has + * not requested that msg.sender become the new owner. + */ + function acceptSubscriptionOwnerTransfer(uint64 subId) external; + + /** + * @notice Add a consumer to a VRF subscription. + * @param subId - ID of the subscription + * @param consumer - New consumer which can use the subscription + */ + function addConsumer(uint64 subId, address consumer) external; + + /** + * @notice Remove a consumer from a VRF subscription. + * @param subId - ID of the subscription + * @param consumer - Consumer to remove from the subscription + */ + function removeConsumer(uint64 subId, address consumer) external; + + /** + * @notice Cancel a subscription + * @param subId - ID of the subscription + * @param to - Where to send the remaining LINK to + */ + function cancelSubscription(uint64 subId, address to) external; + + /* + * @notice Check to see if there exists a request commitment consumers + * for all consumers and keyhashes for a given sub. + * @param subId - ID of the subscription + * @return true if there exists at least one unfulfilled request for the subscription, false + * otherwise. + */ + function pendingRequestExists(uint64 subId) external view returns (bool); +} From 25544444fa053ba25ab1e8ac8c0228a3ea002927 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Fri, 16 Jun 2023 19:19:53 +0100 Subject: [PATCH 02/30] feat(contracts): vrf subscription manager + token interface --- .../src/rng/VRFSubscriptionManagerV2.sol | 173 ++++++++++++++++++ .../src/rng/interfaces/LinkTokenInterface.sol | 28 +++ 2 files changed, 201 insertions(+) create mode 100644 contracts/src/rng/VRFSubscriptionManagerV2.sol create mode 100644 contracts/src/rng/interfaces/LinkTokenInterface.sol diff --git a/contracts/src/rng/VRFSubscriptionManagerV2.sol b/contracts/src/rng/VRFSubscriptionManagerV2.sol new file mode 100644 index 000000000..3eb6a0c96 --- /dev/null +++ b/contracts/src/rng/VRFSubscriptionManagerV2.sol @@ -0,0 +1,173 @@ +//SPDX-License-Identifier: MIT + +/** + * @authors: [@malatrax] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ +pragma solidity 0.8.18; + +import "./interfaces/VRFCoordinatorV2Interface.sol"; +import "./interfaces/LinkTokenInterface.sol"; + +/** + * @title VRF Coordinator Manager + * @author Simon Malatrait + * @dev This contracts implements a subscription manager for using VRF v2 with the Subscription Method. + * @dev It allows to create subscriptions, manage them and consumers. + * @dev LINK Token Arbitrum: https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum + * @dev VRFCoordinatorV2 address: https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet + * @dev For SECURITY CONSIDERATIONS, you might also have a look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol + */ +contract VRFSubscriptionManagerV2 { + // ************************************* // + // * Events * // + // ************************************* // + + /** + * Emitted when LINK tokens are sent from this contract to the current subscription. + * @param subscriptionId ID of the funded subscription + * @param amount Amount of LINK token, in wei. + */ + event SubscriptionFunded(uint64 subscriptionId, uint256 amount); + + /** + * @dev Emitted when the governor withdraws `amount` LINK from the subscription manager. + * @param receiver Address of the receiving address, the governor address + * @param amount Amount of LINK tokens withdrawn, in wei. + */ + event LinkWithdrawn(address indexed receiver, uint256 indexed amount); + + // ************************************* // + // * Storage * // + // ************************************* // + + VRFCoordinatorV2Interface vrfCoordinator; + LinkTokenInterface linkToken; + uint64 subscriptionId; + address governor; + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + require(msg.sender == governor, "Access not allowed: Governor only"); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + /** + * @dev Constructs the Chainlink VRF v2 Subscription Manager. + * @param _governor The Governor of the contract + * @param _vrfCoordinator The address of the VRFCoordinator contract. + * @param _linkToken The address of the LINK token. + */ + constructor(address _governor, address _vrfCoordinator, address _linkToken) { + vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); + linkToken = LinkTokenInterface(_linkToken); + governor = _governor; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /** + * @dev Changes the `vrfCoordinator` storage variable. + * @param _vrfCoordinator The new value for the `vrfCoordinator` storage variable. + */ + function changeVrfCoordinator(address _vrfCoordinator) external onlyByGovernor { + vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); + } + + /** + * @dev Request the ownership transfer of the current subscription to `newOwner` + * @dev The current owner of the subscription is the Subscription Manager contract + * @param newOwner Address of the proposed new owner of the current subscription + * Note: Transferring the ownership should be used when migrating a subscription from one Subscription Manager to another without removing the consumers nor cancelling the subscription + */ + function requestSubscriptionOwnerTransfer(address newOwner) external onlyByGovernor { + vrfCoordinator.requestSubscriptionOwnerTransfer(subscriptionId, newOwner); + } + + /** + * @dev Accept the subscription transfer of ownership. + * @param _subscriptionId ID of the subscription to be accepted. It will override the current subscription if any. + */ + function acceptSubscriptionOwnerTransfer(uint64 _subscriptionId) external onlyByGovernor { + vrfCoordinator.acceptSubscriptionOwnerTransfer(_subscriptionId); + subscriptionId = _subscriptionId; + } + + /** + * @dev Creates a new subscription, overriding the previous one to be manageable by the contract. + */ + function createNewSubscription() external onlyByGovernor { + subscriptionId = vrfCoordinator.createSubscription(); + } + + /** + * @dev Funds the current subscription by `amount` LINK tokens. + * @param amount Amount of LINK token in wei. + */ + function topUpSubscription(uint256 amount) external { + linkToken.transferAndCall(address(vrfCoordinator), amount, abi.encode(subscriptionId)); + emit SubscriptionFunded(subscriptionId, amount); + } + + /** + * @dev Add a Consumer to the subscription. + * @param consumer Address of the Consumer contract added to the subscription. + */ + function addConsumer(address consumer) external onlyByGovernor { + // Add a consumer contract to the subscription. + vrfCoordinator.addConsumer(subscriptionId, consumer); + } + + /** + * @dev Removes a Consumer to the subscription + * @param consumer Address of the Consumer contract removed from the subscription. + */ + function removeConsumer(address consumer) external onlyByGovernor { + // Remove a consumer contract from the subscription. + vrfCoordinator.removeConsumer(subscriptionId, consumer); + } + + /** + * @dev Cancel the current subscription and send the remaining LINK of the subscription to the governor. + */ + function cancelSubscriptionToGovernor() external onlyByGovernor { + vrfCoordinator.cancelSubscription(subscriptionId, governor); + subscriptionId = 0; + } + + /** + * @dev Transfers `amount` LINK tokens of the Subscription Manager (this contract) to the governor. + * @param amount Amount of LINK token in wei. + */ + function withdrawLinkToGovernor(uint256 amount) external onlyByGovernor { + linkToken.transfer(governor, amount); + emit LinkWithdrawn(governor, amount); + } + + /** + * @dev Returns information on the current subscription + * @return balance LINK token balance of the current subscription. + * @return reqCount Number of requests made to the subscription. + * @return owner Address of the current owner of the subscription. + * @return consumers List of consumers subscribed to the current subscription. + */ + function getSubscription() + external + view + returns (uint96 balance, uint64 reqCount, address owner, address[] memory consumers) + { + (balance, reqCount, owner, consumers) = vrfCoordinator.getSubscription(subscriptionId); + } +} diff --git a/contracts/src/rng/interfaces/LinkTokenInterface.sol b/contracts/src/rng/interfaces/LinkTokenInterface.sol new file mode 100644 index 000000000..203f8684c --- /dev/null +++ b/contracts/src/rng/interfaces/LinkTokenInterface.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface LinkTokenInterface { + function allowance(address owner, address spender) external view returns (uint256 remaining); + + function approve(address spender, uint256 value) external returns (bool success); + + function balanceOf(address owner) external view returns (uint256 balance); + + function decimals() external view returns (uint8 decimalPlaces); + + function decreaseApproval(address spender, uint256 addedValue) external returns (bool success); + + function increaseApproval(address spender, uint256 subtractedValue) external; + + function name() external view returns (string memory tokenName); + + function symbol() external view returns (string memory tokenSymbol); + + function totalSupply() external view returns (uint256 totalTokensIssued); + + function transfer(address to, uint256 value) external returns (bool success); + + function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool success); + + function transferFrom(address from, address to, uint256 value) external returns (bool success); +} From 69d9844ed68f91f5d95517070b8c57164afbc1f9 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Fri, 16 Jun 2023 19:22:09 +0100 Subject: [PATCH 03/30] test(contracts): chainlink vrf coordinator mock --- .../src/rng/mock/VRFCoordinatorV2Mock.sol | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 contracts/src/rng/mock/VRFCoordinatorV2Mock.sol diff --git a/contracts/src/rng/mock/VRFCoordinatorV2Mock.sol b/contracts/src/rng/mock/VRFCoordinatorV2Mock.sol new file mode 100644 index 000000000..70fad48f6 --- /dev/null +++ b/contracts/src/rng/mock/VRFCoordinatorV2Mock.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: MIT +// A mock for testing code that relies on VRFCoordinatorV2. +pragma solidity ^0.8.4; + +import "../interfaces/LinkTokenInterface.sol"; +import "../interfaces/VRFCoordinatorV2Interface.sol"; +import "../VRFConsumerBaseV2.sol"; + +contract VRFCoordinatorV2Mock is VRFCoordinatorV2Interface { + uint96 public immutable BASE_FEE; + uint96 public immutable GAS_PRICE_LINK; + uint16 public immutable MAX_CONSUMERS = 100; + + error InvalidSubscription(); + error InsufficientBalance(); + error MustBeSubOwner(address owner); + error TooManyConsumers(); + error InvalidConsumer(); + error InvalidRandomWords(); + + event RandomWordsRequested( + bytes32 indexed keyHash, + uint256 requestId, + uint256 preSeed, + uint64 indexed subId, + uint16 minimumRequestConfirmations, + uint32 callbackGasLimit, + uint32 numWords, + address indexed sender + ); + event RandomWordsFulfilled(uint256 indexed requestId, uint256 outputSeed, uint96 payment, bool success); + event SubscriptionCreated(uint64 indexed subId, address owner); + event SubscriptionFunded(uint64 indexed subId, uint256 oldBalance, uint256 newBalance); + event SubscriptionCanceled(uint64 indexed subId, address to, uint256 amount); + event ConsumerAdded(uint64 indexed subId, address consumer); + event ConsumerRemoved(uint64 indexed subId, address consumer); + + uint64 s_currentSubId; + uint256 s_nextRequestId = 1; + uint256 s_nextPreSeed = 100; + struct Subscription { + address owner; + uint96 balance; + } + mapping(uint64 => Subscription) s_subscriptions; /* subId */ /* subscription */ + mapping(uint64 => address[]) s_consumers; /* subId */ /* consumers */ + + struct Request { + uint64 subId; + uint32 callbackGasLimit; + uint32 numWords; + } + mapping(uint256 => Request) s_requests; /* requestId */ /* request */ + + constructor(uint96 _baseFee, uint96 _gasPriceLink) { + BASE_FEE = _baseFee; + GAS_PRICE_LINK = _gasPriceLink; + } + + function consumerIsAdded(uint64 _subId, address _consumer) public view returns (bool) { + address[] memory consumers = s_consumers[_subId]; + for (uint256 i = 0; i < consumers.length; i++) { + if (consumers[i] == _consumer) { + return true; + } + } + return false; + } + + modifier onlyValidConsumer(uint64 _subId, address _consumer) { + if (!consumerIsAdded(_subId, _consumer)) { + revert InvalidConsumer(); + } + _; + } + + /** + * @notice fulfillRandomWords fulfills the given request, sending the random words to the supplied + * @notice consumer. + * + * @dev This mock uses a simplified formula for calculating payment amount and gas usage, and does + * @dev not account for all edge cases handled in the real VRF coordinator. When making requests + * @dev against the real coordinator a small amount of additional LINK is required. + * + * @param _requestId the request to fulfill + * @param _consumer the VRF randomness consumer to send the result to + */ + function fulfillRandomWords(uint256 _requestId, address _consumer) external { + fulfillRandomWordsWithOverride(_requestId, _consumer, new uint256[](0)); + } + + /** + * @notice fulfillRandomWordsWithOverride allows the user to pass in their own random words. + * + * @param _requestId the request to fulfill + * @param _consumer the VRF randomness consumer to send the result to + * @param _words user-provided random words + */ + function fulfillRandomWordsWithOverride(uint256 _requestId, address _consumer, uint256[] memory _words) public { + uint256 startGas = gasleft(); + if (s_requests[_requestId].subId == 0) { + revert("nonexistent request"); + } + Request memory req = s_requests[_requestId]; + + if (_words.length == 0) { + _words = new uint256[](req.numWords); + for (uint256 i = 0; i < req.numWords; i++) { + _words[i] = uint256(keccak256(abi.encode(_requestId, i))); + } + } else if (_words.length != req.numWords) { + revert InvalidRandomWords(); + } + + VRFConsumerBaseV2 v; + bytes memory callReq = abi.encodeWithSelector(v.rawFulfillRandomWords.selector, _requestId, _words); + (bool success, ) = _consumer.call{gas: req.callbackGasLimit}(callReq); + + uint96 payment = uint96(BASE_FEE + ((startGas - gasleft()) * GAS_PRICE_LINK)); + if (s_subscriptions[req.subId].balance < payment) { + revert InsufficientBalance(); + } + s_subscriptions[req.subId].balance -= payment; + delete (s_requests[_requestId]); + emit RandomWordsFulfilled(_requestId, _requestId, payment, success); + } + + /** + * @notice fundSubscription allows funding a subscription with an arbitrary amount for testing. + * + * @param _subId the subscription to fund + * @param _amount the amount to fund + */ + function fundSubscription(uint64 _subId, uint96 _amount) public { + if (s_subscriptions[_subId].owner == address(0)) { + revert InvalidSubscription(); + } + uint96 oldBalance = s_subscriptions[_subId].balance; + s_subscriptions[_subId].balance += _amount; + emit SubscriptionFunded(_subId, oldBalance, oldBalance + _amount); + } + + function requestRandomWords( + bytes32 _keyHash, + uint64 _subId, + uint16 _minimumRequestConfirmations, + uint32 _callbackGasLimit, + uint32 _numWords + ) external override onlyValidConsumer(_subId, msg.sender) returns (uint256) { + if (s_subscriptions[_subId].owner == address(0)) { + revert InvalidSubscription(); + } + + uint256 requestId = s_nextRequestId++; + uint256 preSeed = s_nextPreSeed++; + + s_requests[requestId] = Request({subId: _subId, callbackGasLimit: _callbackGasLimit, numWords: _numWords}); + + emit RandomWordsRequested( + _keyHash, + requestId, + preSeed, + _subId, + _minimumRequestConfirmations, + _callbackGasLimit, + _numWords, + msg.sender + ); + return requestId; + } + + function createSubscription() external override returns (uint64 _subId) { + s_currentSubId++; + s_subscriptions[s_currentSubId] = Subscription({owner: msg.sender, balance: 0}); + emit SubscriptionCreated(s_currentSubId, msg.sender); + return s_currentSubId; + } + + function getSubscription( + uint64 _subId + ) external view override returns (uint96 balance, uint64 reqCount, address owner, address[] memory consumers) { + if (s_subscriptions[_subId].owner == address(0)) { + revert InvalidSubscription(); + } + return (s_subscriptions[_subId].balance, 0, s_subscriptions[_subId].owner, s_consumers[_subId]); + } + + function cancelSubscription(uint64 _subId, address _to) external override onlySubOwner(_subId) { + emit SubscriptionCanceled(_subId, _to, s_subscriptions[_subId].balance); + delete (s_subscriptions[_subId]); + } + + modifier onlySubOwner(uint64 _subId) { + address owner = s_subscriptions[_subId].owner; + if (owner == address(0)) { + revert InvalidSubscription(); + } + if (msg.sender != owner) { + revert MustBeSubOwner(owner); + } + _; + } + + function getRequestConfig() external pure override returns (uint16, uint32, bytes32[] memory) { + return (3, 2000000, new bytes32[](0)); + } + + function addConsumer(uint64 _subId, address _consumer) external override onlySubOwner(_subId) { + if (s_consumers[_subId].length == MAX_CONSUMERS) { + revert TooManyConsumers(); + } + + if (consumerIsAdded(_subId, _consumer)) { + return; + } + + s_consumers[_subId].push(_consumer); + emit ConsumerAdded(_subId, _consumer); + } + + function removeConsumer( + uint64 _subId, + address _consumer + ) external override onlySubOwner(_subId) onlyValidConsumer(_subId, _consumer) { + address[] storage consumers = s_consumers[_subId]; + for (uint256 i = 0; i < consumers.length; i++) { + if (consumers[i] == _consumer) { + address last = consumers[consumers.length - 1]; + consumers[i] = last; + consumers.pop(); + break; + } + } + + emit ConsumerRemoved(_subId, _consumer); + } + + function getConfig() + external + view + returns ( + uint16 minimumRequestConfirmations, + uint32 maxGasLimit, + uint32 stalenessSeconds, + uint32 gasAfterPaymentCalculation + ) + { + return (4, 2_500_000, 2_700, 33285); + } + + function getFeeConfig() + external + view + returns ( + uint32 fulfillmentFlatFeeLinkPPMTier1, + uint32 fulfillmentFlatFeeLinkPPMTier2, + uint32 fulfillmentFlatFeeLinkPPMTier3, + uint32 fulfillmentFlatFeeLinkPPMTier4, + uint32 fulfillmentFlatFeeLinkPPMTier5, + uint24 reqsForTier2, + uint24 reqsForTier3, + uint24 reqsForTier4, + uint24 reqsForTier5 + ) + { + return ( + 100000, // 0.1 LINK + 100000, // 0.1 LINK + 100000, // 0.1 LINK + 100000, // 0.1 LINK + 100000, // 0.1 LINK + 0, + 0, + 0, + 0 + ); + } + + function getFallbackWeiPerUnitLink() external view returns (int256) { + return 4000000000000000; // 0.004 Ether + } + + function requestSubscriptionOwnerTransfer(uint64 _subId, address _newOwner) external pure override { + revert("not implemented"); + } + + function acceptSubscriptionOwnerTransfer(uint64 _subId) external pure override { + revert("not implemented"); + } + + function pendingRequestExists(uint64 subId) public view override returns (bool) { + revert("not implemented"); + } +} From 03cd8cae581b49e7b999d963bebcfcf80c40de08 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Fri, 16 Jun 2023 19:22:46 +0100 Subject: [PATCH 04/30] test(contracts): interface for mock coordinator --- .../mock/VRFCoordinatorV2InterfaceMock.sol | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 contracts/src/rng/mock/VRFCoordinatorV2InterfaceMock.sol diff --git a/contracts/src/rng/mock/VRFCoordinatorV2InterfaceMock.sol b/contracts/src/rng/mock/VRFCoordinatorV2InterfaceMock.sol new file mode 100644 index 000000000..e6e73ca8e --- /dev/null +++ b/contracts/src/rng/mock/VRFCoordinatorV2InterfaceMock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +// A mock for testing code that relies on VRFCoordinatorV2. +pragma solidity ^0.8.4; + +interface VRFCoordinatorV2InterfaceMock { + function consumerIsAdded(uint64 _subId, address _consumer) external view returns (bool); + + function fulfillRandomWords(uint256 _requestId, address _consumer) external; + + function fundSubscription(uint64 _subId, uint96 _amount) external; + + function requestRandomWords( + bytes32 _keyHash, + uint64 _subId, + uint16 _minimumRequestConfirmations, + uint32 _callbackGasLimit, + uint32 _numWords + ) external returns (uint256); + + function createSubscription() external returns (uint64 _subId); + + function getSubscription( + uint64 _subId + ) external view returns (uint96 balance, uint64 reqCount, address owner, address[] memory consumers); + + function cancelSubscription(uint64 _subId, address _to) external; + + function addConsumer(uint64 _subId, address _consumer) external; + + function removeConsumer(uint64 _subId, address _consumer) external; +} From 81d0c37b9311d7331489626e9b5470ee882df977 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Fri, 16 Jun 2023 19:23:09 +0100 Subject: [PATCH 05/30] test(contracts): vrf consumer mock --- contracts/src/rng/mock/VRFConsumerV2Mock.sol | 225 +++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 contracts/src/rng/mock/VRFConsumerV2Mock.sol diff --git a/contracts/src/rng/mock/VRFConsumerV2Mock.sol b/contracts/src/rng/mock/VRFConsumerV2Mock.sol new file mode 100644 index 000000000..4b374f4d9 --- /dev/null +++ b/contracts/src/rng/mock/VRFConsumerV2Mock.sol @@ -0,0 +1,225 @@ +//SPDX-License-Identifier: MIT + +/** + * @authors: [@malatrax] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ +pragma solidity 0.8.18; + +import "../RNG.sol"; +import "../VRFConsumerBaseV2.sol"; +import "./VRFCoordinatorV2InterfaceMock.sol"; + +// Interface to call passPhase in the callback function +interface ISortitionModule { + function passPhase() external; +} + +/** + * @title Random Number Generator using Chainlink Verifiable Resolution Mechanism v2 on Arbitrum - Subscription Method - Consumer + * @author Simon Malatrait + * @dev This contract implements the RNG standard and inherits from VRFConsumerBaseV2 to use Chainlink Verifiable Randomness Mechanism. + * @dev It allows to store the random number associated to the requests made. + * @dev Chainlink Subscription Method Documentation: https://docs.chain.link/vrf/v2/subscription + * @dev Chainlink Subscription Method Network Parameters: https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet + * @dev For SECURITY CONSIDERATIONS, you might also have look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol + * @dev For SECURITY CONSIDERATIONS, you might also have look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol + */ +contract VRFConsumerV2Mock is VRFConsumerBaseV2, RNG { + // ************************************* // + // * Events * // + // ************************************* // + + /** + * @dev Emitted when a request is sent to the VRF Coordinator + * @param requestId The ID of the request + * @param numWords The number of random values requested + */ + event RequestSent(uint256 indexed requestId, uint32 numWords); + + /** + * Emitted when a request has been fulfilled. + * @param requestId The ID of the request + * @param randomWords The random values answering the request. + */ + event RequestFulfilled(uint256 indexed requestId, uint256[] randomWords); + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; + bytes32 public keyHash; + VRFCoordinatorV2InterfaceMock vrfCoordinator; + uint64 public subscriptionId; + uint32 public callbackGasLimit; + ISortitionModule sortitionModule; + uint32 public numWords; + uint16 public requestConfirmations; + uint256 public lastRequestId; + + mapping(uint256 => uint256) public requestsToRandomWords; // s_requests[requestId] = randomWord + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyBySortitionModule() { + require(msg.sender == address(sortitionModule), "Access not allowed: SortitionModule only"); + _; + } + + modifier onlyByGovernor() { + require(msg.sender == governor, "Access not allowed: Governor only"); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + /** + * @dev Constructs the ChainlinkRNG contract. + * @param _governor The Governor of the contract. + * @param _vrfCoordinator The address of the VRFCoordinator contract. + * @param _sortitionModule The address of the SortitionModule contract. + * @param _keyHash The gas lane key hash value - Defines the maximum gas price you are willing to pay for a request in wei (ID of the off-chain VRF job). + * @param _subscriptionId The unique identifier of the subscription used for funding requests. + * @param _requestConfirmations How many confirmations the Chainlink node should wait before responding. + * @param _callbackGasLimit The limit for how much gas to use for the callback request to the contract's fulfillRandomWords() function. + * @param _numWords How many random values to request. + * @dev https://docs.chain.link/vrf/v2/subscription/examples/get-a-random-number#analyzing-the-contract + */ + constructor( + address _governor, + address _vrfCoordinator, + address _sortitionModule, + bytes32 _keyHash, + uint64 _subscriptionId, + uint16 _requestConfirmations, + uint32 _callbackGasLimit, + uint32 _numWords + ) VRFConsumerBaseV2(_vrfCoordinator) { + governor = _governor; + vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); + sortitionModule = ISortitionModule(_sortitionModule); + keyHash = _keyHash; + subscriptionId = _subscriptionId; + requestConfirmations = _requestConfirmations; + callbackGasLimit = _callbackGasLimit; + numWords = _numWords; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /** + * @dev Changes the `vrfCoordinator` storage variable. + * @param _vrfCoordinator The new value for the `vrfCoordinator` storage variable. + */ + function changeVrfCoordinator(address _vrfCoordinator) external onlyByGovernor { + vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); + } + + /** + * @dev Changes the `sortitionModule` storage variable. + * @param _sortitionModule The new value for the `sortitionModule` storage variable. + */ + function changeSortitionModule(address _sortitionModule) external onlyByGovernor { + sortitionModule = ISortitionModule(_sortitionModule); + } + + /** + * @dev Changes the `keyHash` storage variable. + * @param _keyHash The new value for the `keyHash` storage variable. + */ + function changeKeyHash(bytes32 _keyHash) external onlyByGovernor { + keyHash = _keyHash; + } + + /** + * @dev Changes the `subscriptionId` storage variable. + * @param _subscriptionId The new value for the `subscriptionId` storage variable. + */ + function changeSubscriptionId(uint64 _subscriptionId) external onlyByGovernor { + subscriptionId = _subscriptionId; + } + + /** + * @dev Changes the `requestConfirmations` storage variable. + * @param _requestConfirmations The new value for the `requestConfirmations` storage variable. + */ + function changeRequestConfirmations(uint16 _requestConfirmations) external onlyByGovernor { + requestConfirmations = _requestConfirmations; + } + + /** + * @dev Changes the `callbackGasLimit` storage variable. + * @param _callbackGasLimit The new value for the `callbackGasLimit` storage variable. + */ + function changeCallbackGasLimit(uint32 _callbackGasLimit) external onlyByGovernor { + callbackGasLimit = _callbackGasLimit; + } + + /** + * @dev Changes the `numWords` storage variable. + * @param _numWords The new value for the `numWords` storage variable. + */ + function changeNumWord(uint32 _numWords) external onlyByGovernor { + numWords = _numWords; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /** + * @dev Submit a request to the VRF Coordinator contract with the specified parameters. + * @dev Assumes the subscription is funded sufficiently; "Words" refers to unit of data in Computer Science + * Note Buffer of one requestId, as in RandomizerRNG, which should be enough with the callback function. + */ + function requestRandomness(uint256 /* _block */) external onlyBySortitionModule { + // Will revert if subscription is not set and funded. + uint256 requestId = vrfCoordinator.requestRandomWords( + keyHash, + subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords + ); + lastRequestId = requestId; + emit RequestSent(requestId, numWords); + } + + // ************************************* // + // * Internal * // + // ************************************* // + + /** + * @dev Callback function used by VRF Coordinator + * @dev Stores the random number given by the VRF Coordinator and calls passPhase function on SortitionModule. + * @param _requestId The same request Id initially returned by `vrfCoordinator.requestRandomWords` and stored in the `lastRequestId` storage variable. + * @param _randomWords - array of random results from VRF Coordinator + */ + function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { + requestsToRandomWords[_requestId] = _randomWords[0]; + emit RequestFulfilled(_requestId, _randomWords); + sortitionModule.passPhase(); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * @dev Get the random value associated to `lastRequestId` + * @return randomNumber The random number. If the value is not ready or has not been required it returns 0. + */ + function receiveRandomness(uint256 /* _block */) external view returns (uint256 randomNumber) { + randomNumber = requestsToRandomWords[lastRequestId]; + } +} From f5f3a55304df511a943bb54a01e472f21509a2a7 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Fri, 16 Jun 2023 19:23:25 +0100 Subject: [PATCH 06/30] test(contracts): vrf subscription manager mock --- .../rng/mock/VRFSubscriptionManagerV2Mock.sol | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol diff --git a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol new file mode 100644 index 000000000..769eee965 --- /dev/null +++ b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol @@ -0,0 +1,144 @@ +//SPDX-License-Identifier: MIT + +/** + * @authors: [@malatrax] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ +pragma solidity 0.8.18; + +import "./VRFCoordinatorV2InterfaceMock.sol"; +import "../interfaces/LinkTokenInterface.sol"; + +/** + * @title VRF Coordinator Manager Mock + * @author Simon Malatrait + * @dev This contracts implements a subscription manager for using VRF v2 with the Subscription Method. + * @dev It allows to create subscriptions, manage them and consumers. + * @dev LINK Token Arbitrum: https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum + * @dev VRFCoordinatorV2 address: https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet + * @dev For SECURITY CONSIDERATIONS, you might also have a look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol + */ +contract VRFSubscriptionManagerV2Mock { + // ************************************* // + // * Events * // + // ************************************* // + + /** + * Emitted when LINK tokens are sent from this contract to the current subscription. + * @param subscriptionId ID of the funded subscription + * @param amount Amount of LINK token, in wei. + */ + event SubscriptionFunded(uint64 subscriptionId, uint256 amount); + + /** + * @dev Emitted when the governor withdraws `amount` LINK from the subscription manager. + * @param receiver Address of the receiving address, the governor address + * @param amount Amount of LINK tokens withdrawn, in wei. + */ + event LinkWithdrawn(address indexed receiver, uint256 indexed amount); + + // ************************************* // + // * Storage * // + // ************************************* // + + VRFCoordinatorV2InterfaceMock vrfCoordinator; + LinkTokenInterface linkToken; + uint64 subscriptionId; + address governor; + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + require(msg.sender == governor, "Access not allowed: Governor only"); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + /** + * @dev Constructs the Chainlink VRF v2 Subscription Manager. + * @param _governor The Governor of the contract + * @param _vrfCoordinator The address of the VRFCoordinator contract. + * @param _linkToken The address of the LINK token. + */ + constructor(address _governor, address _vrfCoordinator, address _linkToken) { + vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); + linkToken = LinkTokenInterface(_linkToken); + governor = _governor; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /** + * @dev Changes the `vrfCoordinator` storage variable. + * @param _vrfCoordinator The new value for the `vrfCoordinator` storage variable. + */ + function changeVrfCoordinator(address _vrfCoordinator) external onlyByGovernor { + vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); + } + + /** + * @dev Creates a new subscription, overriding the previous one to be manageable by the contract. + */ + function createNewSubscription() external onlyByGovernor { + subscriptionId = vrfCoordinator.createSubscription(); + } + + /** + * @dev Funds the current subscription by `amount` LINK tokens. + * @param amount Amount of LINK token in wei. + */ + function topUpSubscription(uint96 amount) external { + vrfCoordinator.fundSubscription(subscriptionId, amount); + } + + /** + * @dev Add a Consumer to the subscription. + * @param consumer Address of the Consumer contract added to the subscription. + */ + function addConsumer(address consumer) external onlyByGovernor { + // Add a consumer contract to the subscription. + vrfCoordinator.addConsumer(subscriptionId, consumer); + } + + /** + * @dev Removes a Consumer to the subscription + * @param consumer Address of the Consumer contract removed from the subscription. + */ + function removeConsumer(address consumer) external onlyByGovernor { + // Remove a consumer contract from the subscription. + vrfCoordinator.removeConsumer(subscriptionId, consumer); + } + + /** + * @dev Cancel the current subscription and send the remaining LINK of the subscription to the governor. + */ + function cancelSubscriptionToGovernor() external onlyByGovernor { + vrfCoordinator.cancelSubscription(subscriptionId, governor); + subscriptionId = 0; + } + + /** + * @dev Returns information on the current subscription + * @return balance LINK token balance of the current subscription. + * @return reqCount Number of requests made to the subscription. + * @return owner Address of the current owner of the subscription. + * @return consumers List of consumers subscribed to the current subscription. + */ + function getSubscription() + external + view + returns (uint96 balance, uint64 reqCount, address owner, address[] memory consumers) + { + (balance, reqCount, owner, consumers) = vrfCoordinator.getSubscription(subscriptionId); + } +} From 298a67fe58b1927ec03483dcae0be947386f232b Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Mon, 19 Jun 2023 11:03:52 +0100 Subject: [PATCH 07/30] test(contracts): removed link token from mock --- .../src/rng/mock/VRFSubscriptionManagerV2Mock.sol | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol index 769eee965..ad4e678b9 100644 --- a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol +++ b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol @@ -10,14 +10,12 @@ pragma solidity 0.8.18; import "./VRFCoordinatorV2InterfaceMock.sol"; -import "../interfaces/LinkTokenInterface.sol"; /** * @title VRF Coordinator Manager Mock * @author Simon Malatrait * @dev This contracts implements a subscription manager for using VRF v2 with the Subscription Method. * @dev It allows to create subscriptions, manage them and consumers. - * @dev LINK Token Arbitrum: https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum * @dev VRFCoordinatorV2 address: https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet * @dev For SECURITY CONSIDERATIONS, you might also have a look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol */ @@ -33,19 +31,11 @@ contract VRFSubscriptionManagerV2Mock { */ event SubscriptionFunded(uint64 subscriptionId, uint256 amount); - /** - * @dev Emitted when the governor withdraws `amount` LINK from the subscription manager. - * @param receiver Address of the receiving address, the governor address - * @param amount Amount of LINK tokens withdrawn, in wei. - */ - event LinkWithdrawn(address indexed receiver, uint256 indexed amount); - // ************************************* // // * Storage * // // ************************************* // VRFCoordinatorV2InterfaceMock vrfCoordinator; - LinkTokenInterface linkToken; uint64 subscriptionId; address governor; @@ -66,11 +56,9 @@ contract VRFSubscriptionManagerV2Mock { * @dev Constructs the Chainlink VRF v2 Subscription Manager. * @param _governor The Governor of the contract * @param _vrfCoordinator The address of the VRFCoordinator contract. - * @param _linkToken The address of the LINK token. */ - constructor(address _governor, address _vrfCoordinator, address _linkToken) { + constructor(address _governor, address _vrfCoordinator) { vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); - linkToken = LinkTokenInterface(_linkToken); governor = _governor; } From dd0310199de81f46c165152c3a7f061f2d02ca83 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Mon, 19 Jun 2023 11:05:32 +0100 Subject: [PATCH 08/30] style(contracts): add missing missing function section comments --- contracts/src/rng/VRFSubscriptionManagerV2.sol | 8 ++++++++ contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/contracts/src/rng/VRFSubscriptionManagerV2.sol b/contracts/src/rng/VRFSubscriptionManagerV2.sol index 3eb6a0c96..232bde383 100644 --- a/contracts/src/rng/VRFSubscriptionManagerV2.sol +++ b/contracts/src/rng/VRFSubscriptionManagerV2.sol @@ -86,6 +86,10 @@ contract VRFSubscriptionManagerV2 { vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); } + // ************************************* // + // * State Modifiers * // + // ************************************* // + /** * @dev Request the ownership transfer of the current subscription to `newOwner` * @dev The current owner of the subscription is the Subscription Manager contract @@ -156,6 +160,10 @@ contract VRFSubscriptionManagerV2 { emit LinkWithdrawn(governor, amount); } + // ************************************* // + // * Public Views * // + // ************************************* // + /** * @dev Returns information on the current subscription * @return balance LINK token balance of the current subscription. diff --git a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol index ad4e678b9..364096a6a 100644 --- a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol +++ b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol @@ -74,6 +74,10 @@ contract VRFSubscriptionManagerV2Mock { vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); } + // ************************************* // + // * State Modifiers * // + // ************************************* // + /** * @dev Creates a new subscription, overriding the previous one to be manageable by the contract. */ @@ -115,6 +119,10 @@ contract VRFSubscriptionManagerV2Mock { subscriptionId = 0; } + // ************************************* // + // * Public Views * // + // ************************************* // + /** * @dev Returns information on the current subscription * @return balance LINK token balance of the current subscription. From 0485596592f401dba589f9beb35a543f5210c1a4 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Mon, 19 Jun 2023 14:55:04 +0100 Subject: [PATCH 09/30] fix(contracts): add missing storage visibility to public --- contracts/src/rng/VRFConsumerV2.sol | 4 ++-- contracts/src/rng/VRFSubscriptionManagerV2.sol | 8 ++++---- contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/src/rng/VRFConsumerV2.sol b/contracts/src/rng/VRFConsumerV2.sol index 108958fa9..c2447be0d 100644 --- a/contracts/src/rng/VRFConsumerV2.sol +++ b/contracts/src/rng/VRFConsumerV2.sol @@ -53,10 +53,10 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { address public governor; bytes32 public keyHash; - VRFCoordinatorV2Interface vrfCoordinator; + VRFCoordinatorV2Interface public vrfCoordinator; uint64 public subscriptionId; uint32 public callbackGasLimit; - ISortitionModule sortitionModule; + ISortitionModule public sortitionModule; uint32 public numWords; uint16 public requestConfirmations; uint256 public lastRequestId; diff --git a/contracts/src/rng/VRFSubscriptionManagerV2.sol b/contracts/src/rng/VRFSubscriptionManagerV2.sol index 232bde383..f96d31dde 100644 --- a/contracts/src/rng/VRFSubscriptionManagerV2.sol +++ b/contracts/src/rng/VRFSubscriptionManagerV2.sol @@ -44,10 +44,10 @@ contract VRFSubscriptionManagerV2 { // * Storage * // // ************************************* // - VRFCoordinatorV2Interface vrfCoordinator; - LinkTokenInterface linkToken; - uint64 subscriptionId; - address governor; + VRFCoordinatorV2Interface public vrfCoordinator; + LinkTokenInterface public linkToken; + uint64 public subscriptionId; + address public governor; // ************************************* // // * Function Modifiers * // diff --git a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol index 364096a6a..96108a8af 100644 --- a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol +++ b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol @@ -35,9 +35,9 @@ contract VRFSubscriptionManagerV2Mock { // * Storage * // // ************************************* // - VRFCoordinatorV2InterfaceMock vrfCoordinator; - uint64 subscriptionId; - address governor; + VRFCoordinatorV2InterfaceMock public vrfCoordinator; + uint64 public subscriptionId; + address public governor; // ************************************* // // * Function Modifiers * // From 4b01e8a2980a608851a1fdfa0f253ae5fcacbf6c Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Mon, 19 Jun 2023 14:55:56 +0100 Subject: [PATCH 10/30] test(contracts): add deploy scripts for vrf --- contracts/deploy/00-home-chain-arbitration.ts | 82 ++++++++++++++++++- contracts/deploy/00-rng.ts | 76 ++++++++++++++++- 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index bf5b961bb..24a49248f 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -4,6 +4,7 @@ import { BigNumber } from "ethers"; import getContractAddress from "./utils/getContractAddress"; import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped, isDevnet } from "./utils"; +import { VRFSubscriptionManagerV2Mock, SortitionModule } from "../typechain-types"; const pnkByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x330bD769382cFc6d50175903434CCC8D206DCAE5"], @@ -18,6 +19,21 @@ const randomizerByChain = new Map([ const daiByChain = new Map([[HomeChains.ARBITRUM_ONE, "??"]]); const wethByChain = new Map([[HomeChains.ARBITRUM_ONE, "??"]]); +const linkByChain = new Map([ + [HomeChains.ARBITRUM_ONE, "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"], + [HomeChains.ARBITRUM_GOERLI, "0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28"], +]); + +const keyHashByChain = new Map([ + [HomeChains.ARBITRUM_ONE, "0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63"], // 30 gwei key Hash + [HomeChains.ARBITRUM_GOERLI, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], + [HomeChains.HARDHAT, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], // arbitrary value +]); + +const vrfCoordinatorByChain = new Map([ + [HomeChains.ARBITRUM_ONE, "0x41034678D6C633D8a95c75e1138A360a28bA15d1"], + [HomeChains.ARBITRUM_GOERLI, "0x6D80646bEAdd07cE68cab36c27c626790bBcf17f"], +]); const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; @@ -42,7 +58,18 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const erc20Address = await deployERC20AndFaucet(hre, deployer, "WETH"); wethByChain.set(HomeChains[HomeChains[chainId]], erc20Address); } - + if (chainId === HomeChains.HARDHAT) { + vrfCoordinatorByChain.set( + HomeChains.HARDHAT, + ( + await deploy("VRFCoordinatorV2Mock", { + from: deployer, + args: [BigNumber.from(10).pow(18), 1000000000], // base_fee: 1 LINK, gas_price_link: 0.000000001 LINK per gas, from chainlink mock scripts + log: true, + }) + ).address + ); + } if (!randomizerByChain.get(chainId)) { const randomizerMock = await deploy("RandomizerMock", { from: deployer, @@ -119,6 +146,59 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", pnk, 12225583, 12); await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", dai, 60327783, 11); await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", weth, 1, 1); + await deploy("DisputeResolver", { + from: deployer, + args: [klerosCore.address], + log: true, + }); + + const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; + const keyHash = keyHashByChain.get(Number(await getChainId())) ?? AddressZero; + const requestConfirmations = 3; + const callbackGasLimit = 100000; + const numWords = 1; + const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero; + const vrfSubscriptionManagerDeploy = vrfCoordinator + ? chainId === HomeChains.HARDHAT + ? await deploy("VRFSubscriptionManagerV2Mock", { + from: deployer, + args: [deployer, vrfCoordinator], + log: true, + }) + : await deploy("VRFSubscriptionManagerV2", { + from: deployer, + args: [deployer, vrfCoordinator, link], + log: true, + }) + : AddressZero; + + if (vrfSubscriptionManagerDeploy) { + if (chainId === HomeChains.HARDHAT) { + const vrfSubscriptionManager = (await hre.ethers.getContract( + "VRFSubscriptionManagerV2Mock" + )) as VRFSubscriptionManagerV2Mock; + await vrfSubscriptionManager.createNewSubscription(); + await vrfSubscriptionManager.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK + const subId = await vrfSubscriptionManager.subscriptionId(); + const vrfConsumer = await deploy("VRFConsumerV2", { + from: deployer, + args: [ + deployer, + vrfCoordinator, + sortitionModule.address, + keyHash, + subId, + requestConfirmations, + callbackGasLimit, + numWords, + ], + log: true, + }); + await vrfSubscriptionManager.addConsumer(vrfConsumer.address); + const sortModule = (await hre.ethers.getContract("SortitionModule")) as SortitionModule; + await sortModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + } + } }; deployArbitration.tags = ["Arbitration"]; diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts index 30289b50d..d8bb2ee1b 100644 --- a/contracts/deploy/00-rng.ts +++ b/contracts/deploy/00-rng.ts @@ -1,6 +1,7 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { BigNumber } from "ethers"; import { DeployFunction } from "hardhat-deploy/types"; -import { SortitionModule, RandomizerRNG } from "../typechain-types"; +import { SortitionModule, VRFSubscriptionManagerV2Mock } from "../typechain-types"; import { HomeChains, isSkipped } from "./utils"; import { deployUpgradable } from "./utils/deployUpgradable"; @@ -14,6 +15,22 @@ const randomizerByChain = new Map([ [HomeChains.ARBITRUM_GOERLI, "0x923096Da90a3b60eb7E12723fA2E1547BA9236Bc"], ]); +const linkByChain = new Map([ + [HomeChains.ARBITRUM_ONE, "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"], + [HomeChains.ARBITRUM_GOERLI, "0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28"], +]); + +const keyHashByChain = new Map([ + [HomeChains.ARBITRUM_ONE, "0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63"], // 30 gwei key Hash + [HomeChains.ARBITRUM_GOERLI, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], + [HomeChains.HARDHAT, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], +]); + +const vrfCoordinatorByChain = new Map([ + [HomeChains.ARBITRUM_ONE, "0x41034678D6C633D8a95c75e1138A360a28bA15d1"], + [HomeChains.ARBITRUM_GOERLI, "0x6D80646bEAdd07cE68cab36c27c626790bBcf17f"], +]); + const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; const { deploy, execute } = deployments; @@ -45,6 +62,16 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }) ).address ); + vrfCoordinatorByChain.set( + HomeChains.HARDHAT, + ( + await deploy("VRFCoordinatorV2Mock", { + from: deployer, + args: [BigNumber.from(10).pow(18), 1000000000], // base_fee: 1 LINK, gas_price_link: 0.000000001 LINK per gas, from chainlink mock scripts + log: true, + }) + ).address + ); } const randomizer = randomizerByChain.get(Number(await getChainId())) ?? AddressZero; @@ -57,6 +84,53 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const sortitionModule = (await hre.ethers.getContract("SortitionModule")) as SortitionModule; await sortitionModule.changeRandomNumberGenerator(rng.address, RNG_LOOKAHEAD); + + const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; + const keyHash = keyHashByChain.get(Number(await getChainId())) ?? AddressZero; + const requestConfirmations = 3; + const callbackGasLimit = 100000; + const numWords = 1; + const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero; + const vrfSubscriptionManagerDeploy = vrfCoordinator + ? chainId === HomeChains.HARDHAT + ? await deploy("VRFSubscriptionManagerV2Mock", { + from: deployer, + args: [deployer, vrfCoordinator], + log: true, + }) + : await deploy("VRFSubscriptionManagerV2", { + from: deployer, + args: [deployer, vrfCoordinator, link], + log: true, + }) + : AddressZero; + + if (vrfSubscriptionManagerDeploy) { + if (chainId === HomeChains.HARDHAT) { + const vrfSubscriptionManager = (await hre.ethers.getContract( + "VRFSubscriptionManagerV2Mock" + )) as VRFSubscriptionManagerV2Mock; + await vrfSubscriptionManager.createNewSubscription(); + await vrfSubscriptionManager.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK + const subId = await vrfSubscriptionManager.subscriptionId(); + const vrfConsumer = await deploy("VRFConsumerV2", { + from: deployer, + args: [ + deployer, + vrfCoordinator, + sortitionModule.address, + keyHash, + subId, + requestConfirmations, + callbackGasLimit, + numWords, + ], + log: true, + }); + await vrfSubscriptionManager.addConsumer(vrfConsumer.address); + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + } + } }; deployArbitration.tags = ["RNG"]; From 61801458b0bcf51bc2efbf3597f3dbe508a559a8 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Mon, 19 Jun 2023 14:56:37 +0100 Subject: [PATCH 11/30] test(contracts): modify test to use vrf instead of randomizer --- contracts/test/arbitration/draw.ts | 6 ++++++ contracts/test/arbitration/unstake.ts | 12 ++++++++++-- contracts/test/integration/index.ts | 24 +++++++++++++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index 47be1689f..e52666beb 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -8,6 +8,8 @@ import { HomeGateway, DisputeKitClassic, SortitionModule, + VRFConsumerV2, + VRFCoordinatorV2Mock, } from "../../typechain-types"; import { expect } from "chai"; import { DrawEvent } from "../../typechain-types/src/kleros-v1/kleros-liquid-xdai/XKlerosLiquidV2"; @@ -53,6 +55,8 @@ describe("Draw Benchmark", async () => { const RANDOM = BigNumber.from("61688911660239508166491237672720926005752254046266901728404745669596507231249"); const PARENT_COURT = 1; const CHILD_COURT = 2; + let vrfConsumer; + let vrfCoordinator; beforeEach("Setup", async () => { ({ deployer, relayer } = await getNamedAccounts()); @@ -67,6 +71,8 @@ describe("Draw Benchmark", async () => { homeGateway = (await ethers.getContract("HomeGatewayToEthereum")) as HomeGateway; arbitrable = (await ethers.getContract("ArbitrableExample")) as ArbitrableExample; sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule; + vrfConsumer = (await ethers.getContract("VRFConsumerV2")) as VRFConsumerV2; + vrfCoordinator = (await ethers.getContract("VRFCoordinatorV2Mock")) as VRFCoordinatorV2Mock; parentCourtMinStake = await core.courts(Courts.GENERAL).then((court) => court.minStake); diff --git a/contracts/test/arbitration/unstake.ts b/contracts/test/arbitration/unstake.ts index 671fe3c80..add46f988 100644 --- a/contracts/test/arbitration/unstake.ts +++ b/contracts/test/arbitration/unstake.ts @@ -7,6 +7,8 @@ import { SortitionModule, RandomizerRNG, RandomizerMock, + VRFConsumerV2, + VRFCoordinatorV2Mock, } from "../../typechain-types"; import { expect } from "chai"; @@ -28,6 +30,8 @@ describe("Unstake juror", async () => { let sortitionModule; let rng; let randomizer; + let vrfConsumer; + let vrfCoordinator; beforeEach("Setup", async () => { ({ deployer } = await getNamedAccounts()); @@ -41,6 +45,8 @@ describe("Unstake juror", async () => { sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule; rng = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG; randomizer = (await ethers.getContract("RandomizerMock")) as RandomizerMock; + vrfConsumer = (await ethers.getContract("VRFConsumerV2")) as VRFConsumerV2; + vrfCoordinator = (await ethers.getContract("VRFCoordinatorV2Mock")) as VRFCoordinatorV2Mock; }); it("Unstake inactive juror", async () => { @@ -64,8 +70,10 @@ describe("Unstake juror", async () => { for (let index = 0; index < lookahead; index++) { await network.provider.send("evm_mine"); } - await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); - await sortitionModule.passPhase(); // Generating -> Drawing + // await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); + const requestId = await vrfConsumer.lastRequestId(); + await vrfCoordinator.fulfillRandomWords(requestId, vrfConsumer.address); + // await sortitionModule.passPhase(); // Generating -> Drawing await core.draw(0, 5000); diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index fecfe54f2..707048574 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -13,6 +13,9 @@ import { RandomizerRNG, RandomizerMock, SortitionModule, + VRFConsumerV2, + VRFSubscriptionManagerV2Mock, + VRFCoordinatorV2Mock, } from "../../typechain-types"; /* eslint-disable no-unused-vars */ @@ -39,7 +42,18 @@ describe("Integration tests", async () => { } let deployer; - let rng, randomizer, disputeKit, pnk, core, vea, foreignGateway, arbitrable, homeGateway, sortitionModule; + let rng, + randomizer, + vrfConsumer, + vrfCoordinator, + disputeKit, + pnk, + core, + vea, + foreignGateway, + arbitrable, + homeGateway, + sortitionModule; beforeEach("Setup", async () => { ({ deployer } = await getNamedAccounts()); @@ -49,6 +63,8 @@ describe("Integration tests", async () => { }); rng = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG; randomizer = (await ethers.getContract("RandomizerMock")) as RandomizerMock; + vrfCoordinator = (await ethers.getContract("VRFCoordinatorV2Mock")) as VRFCoordinatorV2Mock; + vrfConsumer = (await ethers.getContract("VRFConsumerV2")) as VRFConsumerV2; disputeKit = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic; pnk = (await ethers.getContract("PNK")) as PNK; core = (await ethers.getContract("KlerosCore")) as KlerosCore; @@ -149,8 +165,10 @@ describe("Integration tests", async () => { await mineBlocks(await sortitionModule.rngLookahead()); // Wait for finality expect(await sortitionModule.phase()).to.equal(Phase.generating); console.log("KC phase: %d", await sortitionModule.phase()); - await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); - await sortitionModule.passPhase(); // Generating -> Drawing + // await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); + const reqId = await vrfConsumer.lastRequestId(); + await vrfCoordinator.fulfillRandomWords(reqId, vrfConsumer.address); + // await sortitionModule.passPhase(); // Generating -> Drawing expect(await sortitionModule.phase()).to.equal(Phase.drawing); console.log("KC phase: %d", await sortitionModule.phase()); From 332b9df1028287f3e6b28c0d45012c2e9ba31e7c Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 07:14:18 +0100 Subject: [PATCH 12/30] refactor: remove unused mock contract --- contracts/src/rng/mock/VRFConsumerV2Mock.sol | 225 ------------------- 1 file changed, 225 deletions(-) delete mode 100644 contracts/src/rng/mock/VRFConsumerV2Mock.sol diff --git a/contracts/src/rng/mock/VRFConsumerV2Mock.sol b/contracts/src/rng/mock/VRFConsumerV2Mock.sol deleted file mode 100644 index 4b374f4d9..000000000 --- a/contracts/src/rng/mock/VRFConsumerV2Mock.sol +++ /dev/null @@ -1,225 +0,0 @@ -//SPDX-License-Identifier: MIT - -/** - * @authors: [@malatrax] - * @reviewers: [] - * @auditors: [] - * @bounties: [] - * @deployments: [] - */ -pragma solidity 0.8.18; - -import "../RNG.sol"; -import "../VRFConsumerBaseV2.sol"; -import "./VRFCoordinatorV2InterfaceMock.sol"; - -// Interface to call passPhase in the callback function -interface ISortitionModule { - function passPhase() external; -} - -/** - * @title Random Number Generator using Chainlink Verifiable Resolution Mechanism v2 on Arbitrum - Subscription Method - Consumer - * @author Simon Malatrait - * @dev This contract implements the RNG standard and inherits from VRFConsumerBaseV2 to use Chainlink Verifiable Randomness Mechanism. - * @dev It allows to store the random number associated to the requests made. - * @dev Chainlink Subscription Method Documentation: https://docs.chain.link/vrf/v2/subscription - * @dev Chainlink Subscription Method Network Parameters: https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet - * @dev For SECURITY CONSIDERATIONS, you might also have look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol - * @dev For SECURITY CONSIDERATIONS, you might also have look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol - */ -contract VRFConsumerV2Mock is VRFConsumerBaseV2, RNG { - // ************************************* // - // * Events * // - // ************************************* // - - /** - * @dev Emitted when a request is sent to the VRF Coordinator - * @param requestId The ID of the request - * @param numWords The number of random values requested - */ - event RequestSent(uint256 indexed requestId, uint32 numWords); - - /** - * Emitted when a request has been fulfilled. - * @param requestId The ID of the request - * @param randomWords The random values answering the request. - */ - event RequestFulfilled(uint256 indexed requestId, uint256[] randomWords); - - // ************************************* // - // * Storage * // - // ************************************* // - - address public governor; - bytes32 public keyHash; - VRFCoordinatorV2InterfaceMock vrfCoordinator; - uint64 public subscriptionId; - uint32 public callbackGasLimit; - ISortitionModule sortitionModule; - uint32 public numWords; - uint16 public requestConfirmations; - uint256 public lastRequestId; - - mapping(uint256 => uint256) public requestsToRandomWords; // s_requests[requestId] = randomWord - - // ************************************* // - // * Function Modifiers * // - // ************************************* // - - modifier onlyBySortitionModule() { - require(msg.sender == address(sortitionModule), "Access not allowed: SortitionModule only"); - _; - } - - modifier onlyByGovernor() { - require(msg.sender == governor, "Access not allowed: Governor only"); - _; - } - - // ************************************* // - // * Constructor * // - // ************************************* // - - /** - * @dev Constructs the ChainlinkRNG contract. - * @param _governor The Governor of the contract. - * @param _vrfCoordinator The address of the VRFCoordinator contract. - * @param _sortitionModule The address of the SortitionModule contract. - * @param _keyHash The gas lane key hash value - Defines the maximum gas price you are willing to pay for a request in wei (ID of the off-chain VRF job). - * @param _subscriptionId The unique identifier of the subscription used for funding requests. - * @param _requestConfirmations How many confirmations the Chainlink node should wait before responding. - * @param _callbackGasLimit The limit for how much gas to use for the callback request to the contract's fulfillRandomWords() function. - * @param _numWords How many random values to request. - * @dev https://docs.chain.link/vrf/v2/subscription/examples/get-a-random-number#analyzing-the-contract - */ - constructor( - address _governor, - address _vrfCoordinator, - address _sortitionModule, - bytes32 _keyHash, - uint64 _subscriptionId, - uint16 _requestConfirmations, - uint32 _callbackGasLimit, - uint32 _numWords - ) VRFConsumerBaseV2(_vrfCoordinator) { - governor = _governor; - vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); - sortitionModule = ISortitionModule(_sortitionModule); - keyHash = _keyHash; - subscriptionId = _subscriptionId; - requestConfirmations = _requestConfirmations; - callbackGasLimit = _callbackGasLimit; - numWords = _numWords; - } - - // ************************************* // - // * Governance * // - // ************************************* // - - /** - * @dev Changes the `vrfCoordinator` storage variable. - * @param _vrfCoordinator The new value for the `vrfCoordinator` storage variable. - */ - function changeVrfCoordinator(address _vrfCoordinator) external onlyByGovernor { - vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); - } - - /** - * @dev Changes the `sortitionModule` storage variable. - * @param _sortitionModule The new value for the `sortitionModule` storage variable. - */ - function changeSortitionModule(address _sortitionModule) external onlyByGovernor { - sortitionModule = ISortitionModule(_sortitionModule); - } - - /** - * @dev Changes the `keyHash` storage variable. - * @param _keyHash The new value for the `keyHash` storage variable. - */ - function changeKeyHash(bytes32 _keyHash) external onlyByGovernor { - keyHash = _keyHash; - } - - /** - * @dev Changes the `subscriptionId` storage variable. - * @param _subscriptionId The new value for the `subscriptionId` storage variable. - */ - function changeSubscriptionId(uint64 _subscriptionId) external onlyByGovernor { - subscriptionId = _subscriptionId; - } - - /** - * @dev Changes the `requestConfirmations` storage variable. - * @param _requestConfirmations The new value for the `requestConfirmations` storage variable. - */ - function changeRequestConfirmations(uint16 _requestConfirmations) external onlyByGovernor { - requestConfirmations = _requestConfirmations; - } - - /** - * @dev Changes the `callbackGasLimit` storage variable. - * @param _callbackGasLimit The new value for the `callbackGasLimit` storage variable. - */ - function changeCallbackGasLimit(uint32 _callbackGasLimit) external onlyByGovernor { - callbackGasLimit = _callbackGasLimit; - } - - /** - * @dev Changes the `numWords` storage variable. - * @param _numWords The new value for the `numWords` storage variable. - */ - function changeNumWord(uint32 _numWords) external onlyByGovernor { - numWords = _numWords; - } - - // ************************************* // - // * State Modifiers * // - // ************************************* // - - /** - * @dev Submit a request to the VRF Coordinator contract with the specified parameters. - * @dev Assumes the subscription is funded sufficiently; "Words" refers to unit of data in Computer Science - * Note Buffer of one requestId, as in RandomizerRNG, which should be enough with the callback function. - */ - function requestRandomness(uint256 /* _block */) external onlyBySortitionModule { - // Will revert if subscription is not set and funded. - uint256 requestId = vrfCoordinator.requestRandomWords( - keyHash, - subscriptionId, - requestConfirmations, - callbackGasLimit, - numWords - ); - lastRequestId = requestId; - emit RequestSent(requestId, numWords); - } - - // ************************************* // - // * Internal * // - // ************************************* // - - /** - * @dev Callback function used by VRF Coordinator - * @dev Stores the random number given by the VRF Coordinator and calls passPhase function on SortitionModule. - * @param _requestId The same request Id initially returned by `vrfCoordinator.requestRandomWords` and stored in the `lastRequestId` storage variable. - * @param _randomWords - array of random results from VRF Coordinator - */ - function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { - requestsToRandomWords[_requestId] = _randomWords[0]; - emit RequestFulfilled(_requestId, _randomWords); - sortitionModule.passPhase(); - } - - // ************************************* // - // * Public Views * // - // ************************************* // - - /** - * @dev Get the random value associated to `lastRequestId` - * @return randomNumber The random number. If the value is not ready or has not been required it returns 0. - */ - function receiveRandomness(uint256 /* _block */) external view returns (uint256 randomNumber) { - randomNumber = requestsToRandomWords[lastRequestId]; - } -} From 784287f0dcc058ba609f6072de69538cc1b55d09 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 07:15:14 +0100 Subject: [PATCH 13/30] chore: add reference to vrf deployed contracts --- contracts/deploy/00-home-chain-arbitration.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 24a49248f..7b3e8552a 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -19,17 +19,20 @@ const randomizerByChain = new Map([ const daiByChain = new Map([[HomeChains.ARBITRUM_ONE, "??"]]); const wethByChain = new Map([[HomeChains.ARBITRUM_ONE, "??"]]); +// https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum const linkByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"], [HomeChains.ARBITRUM_GOERLI, "0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28"], ]); +// https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet const keyHashByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63"], // 30 gwei key Hash [HomeChains.ARBITRUM_GOERLI, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], [HomeChains.HARDHAT, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], // arbitrary value ]); +// https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet const vrfCoordinatorByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x41034678D6C633D8a95c75e1138A360a28bA15d1"], [HomeChains.ARBITRUM_GOERLI, "0x6D80646bEAdd07cE68cab36c27c626790bBcf17f"], From 006d48ab0a5517632dca68e62aa4985b64cc3169 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 07:30:06 +0100 Subject: [PATCH 14/30] chore: add comments on deploy script --- contracts/deploy/00-home-chain-arbitration.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 7b3e8552a..cd99f4a62 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -155,12 +155,13 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); - const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; + const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; // LINK not needed on hardhat local node const keyHash = keyHashByChain.get(Number(await getChainId())) ?? AddressZero; - const requestConfirmations = 3; - const callbackGasLimit = 100000; + const requestConfirmations = 3; // Paramater to be fixed, range [1 ; 200] on Arbitrum + const callbackGasLimit = 100000; // Parameter to be fixed, 50000 on RandomizerRNG but no external call to sortitionModule.passPhase() in the callback const numWords = 1; const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero; + // Deploy the VRF Subscription Manager contract on Arbitrum, a mock contract on Hardhat node or nothing on other networks. const vrfSubscriptionManagerDeploy = vrfCoordinator ? chainId === HomeChains.HARDHAT ? await deploy("VRFSubscriptionManagerV2Mock", { @@ -175,6 +176,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }) : AddressZero; + // Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node if (vrfSubscriptionManagerDeploy) { if (chainId === HomeChains.HARDHAT) { const vrfSubscriptionManager = (await hre.ethers.getContract( From 2e8f43a1615f660a51c750c6b650ed69ed215068 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 07:31:37 +0100 Subject: [PATCH 15/30] refactor: change variable names --- contracts/deploy/00-home-chain-arbitration.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index cd99f4a62..cd07f5faf 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -162,7 +162,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const numWords = 1; const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero; // Deploy the VRF Subscription Manager contract on Arbitrum, a mock contract on Hardhat node or nothing on other networks. - const vrfSubscriptionManagerDeploy = vrfCoordinator + const vrfSubscriptionManager = vrfCoordinator ? chainId === HomeChains.HARDHAT ? await deploy("VRFSubscriptionManagerV2Mock", { from: deployer, @@ -177,14 +177,14 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) : AddressZero; // Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node - if (vrfSubscriptionManagerDeploy) { + if (vrfSubscriptionManager) { if (chainId === HomeChains.HARDHAT) { - const vrfSubscriptionManager = (await hre.ethers.getContract( + const vrfSubscriptionManagerContract = (await hre.ethers.getContract( "VRFSubscriptionManagerV2Mock" )) as VRFSubscriptionManagerV2Mock; - await vrfSubscriptionManager.createNewSubscription(); - await vrfSubscriptionManager.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK - const subId = await vrfSubscriptionManager.subscriptionId(); + await vrfSubscriptionManagerContract.createNewSubscription(); + await vrfSubscriptionManagerContract.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK + const subId = await vrfSubscriptionManagerContract.subscriptionId(); const vrfConsumer = await deploy("VRFConsumerV2", { from: deployer, args: [ @@ -199,9 +199,9 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) ], log: true, }); - await vrfSubscriptionManager.addConsumer(vrfConsumer.address); - const sortModule = (await hre.ethers.getContract("SortitionModule")) as SortitionModule; - await sortModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); + const sortitionModuleContract = (await hre.ethers.getContract("SortitionModule")) as SortitionModule; + await sortitionModuleContract.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); } } }; From 60232c132048a8de7328daa97ec55376a2a1808a Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 07:53:08 +0100 Subject: [PATCH 16/30] test(rng): add distinct tests for Randomizer & VRF --- contracts/deploy/00-home-chain-arbitration.ts | 3 +- contracts/test/arbitration/draw.ts | 60 +++++++++ contracts/test/arbitration/unstake.ts | 52 +++++++- contracts/test/integration/index.ts | 118 +++++++++++++++++- 4 files changed, 221 insertions(+), 12 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index cd07f5faf..baf31c3be 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -178,6 +178,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) // Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node if (vrfSubscriptionManager) { + // The Sortition Module is not changed to the VRF Consumer, it must be done in the test. if (chainId === HomeChains.HARDHAT) { const vrfSubscriptionManagerContract = (await hre.ethers.getContract( "VRFSubscriptionManagerV2Mock" @@ -200,8 +201,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) log: true, }); await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); - const sortitionModuleContract = (await hre.ethers.getContract("SortitionModule")) as SortitionModule; - await sortitionModuleContract.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); } } }; diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index e52666beb..d1600858c 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -415,4 +415,64 @@ describe("Draw Benchmark", async () => { await draw(stake, CHILD_COURT, expectFromDraw, unstake); }); + + it("Draw Benchmark - Chainlink VRF v2", async () => { + const arbitrationCost = ONE_TENTH_ETH.mul(3); + const [bridger] = await ethers.getSigners(); + const RNG_LOOKAHEAD = 20; + + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + + // Stake some jurors + for (let i = 0; i < 16; i++) { + const wallet = ethers.Wallet.createRandom().connect(ethers.provider); + + await bridger.sendTransaction({ + to: wallet.address, + value: ethers.utils.parseEther("10"), + }); + expect(await wallet.getBalance()).to.equal(ethers.utils.parseEther("10")); + + await pnk.transfer(wallet.address, ONE_THOUSAND_PNK.mul(10)); + expect(await pnk.balanceOf(wallet.address)).to.equal(ONE_THOUSAND_PNK.mul(10)); + + await pnk.connect(wallet).approve(core.address, ONE_THOUSAND_PNK.mul(10), { gasLimit: 300000 }); + await core.connect(wallet).setStake(1, ONE_THOUSAND_PNK.mul(10), { gasLimit: 5000000 }); + } + + // Create a dispute + const tx = await arbitrable.createDispute(2, "0x00", 0, { + value: arbitrationCost, + }); + const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); + const [disputeId] = ethers.utils.defaultAbiCoder.decode(["uint"], `0x${trace.returnValue}`); + const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1); + + // Relayer tx + const tx2 = await homeGateway + .connect(await ethers.getSigner(relayer)) + .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { + value: arbitrationCost, + }); + + await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + await sortitionModule.passPhase(); // Staking -> Generating + + const lookahead = await sortitionModule.rngLookahead(); + for (let index = 0; index < lookahead; index++) { + await network.provider.send("evm_mine"); + } + + const requestId = await vrfConsumer.lastRequestId(); // Needed as we emulate the vrfCoordinator manually + await vrfCoordinator.fulfillRandomWords(requestId, vrfConsumer.address); // The callback calls sortitionModule.passPhase(); // Generating -> Drawing + + await expect(core.draw(0, 1000, { gasLimit: 1000000 })) + .to.emit(core, "Draw") + .withArgs(anyValue, 0, 0, 0) + .to.emit(core, "Draw") + .withArgs(anyValue, 0, 0, 1) + .to.emit(core, "Draw") + .withArgs(anyValue, 0, 0, 2); + }); }); diff --git a/contracts/test/arbitration/unstake.ts b/contracts/test/arbitration/unstake.ts index add46f988..818488710 100644 --- a/contracts/test/arbitration/unstake.ts +++ b/contracts/test/arbitration/unstake.ts @@ -49,7 +49,7 @@ describe("Unstake juror", async () => { vrfCoordinator = (await ethers.getContract("VRFCoordinatorV2Mock")) as VRFCoordinatorV2Mock; }); - it("Unstake inactive juror", async () => { + it("Unstake inactive juror - Randomizer", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); await core.createCourt(1, false, ONE_THOUSAND_PNK, 1000, ONE_TENTH_ETH, 3, [0, 0, 0, 0], 3, [1]); // Parent - general court, Classic dispute kit @@ -70,10 +70,52 @@ describe("Unstake juror", async () => { for (let index = 0; index < lookahead; index++) { await network.provider.send("evm_mine"); } - // await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); - const requestId = await vrfConsumer.lastRequestId(); - await vrfCoordinator.fulfillRandomWords(requestId, vrfConsumer.address); - // await sortitionModule.passPhase(); // Generating -> Drawing + + await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); + await sortitionModule.passPhase(); // Generating -> Drawing + + await core.draw(0, 5000); + + await core.passPeriod(0); // Evidence -> Voting + await core.passPeriod(0); // Voting -> Appeal + await core.passPeriod(0); // Appeal -> Execution + + await sortitionModule.passPhase(); // Freezing -> Staking. Change so we don't deal with delayed stakes + + expect(await core.getJurorCourtIDs(deployer)).to.be.deep.equal([BigNumber.from("1"), BigNumber.from("2")]); + + await core.execute(0, 0, 1); // 1 iteration should unstake from both courts + + expect(await core.getJurorCourtIDs(deployer)).to.be.deep.equal([]); + }); + + it("Unstake inactive juror - Chainlink VRF v2", async () => { + const arbitrationCost = ONE_TENTH_ETH.mul(3); + const RNG_LOOKAHEAD = 20; + + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + + await core.createCourt(1, false, ONE_THOUSAND_PNK, 1000, ONE_TENTH_ETH, 3, [0, 0, 0, 0], 3, [1]); // Parent - general court, Classic dispute kit + + await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(4)); + await core.setStake(1, ONE_THOUSAND_PNK.mul(2)); + await core.setStake(2, ONE_THOUSAND_PNK.mul(2)); + + expect(await core.getJurorCourtIDs(deployer)).to.be.deep.equal([BigNumber.from("1"), BigNumber.from("2")]); + + await core.createDispute(2, extraData, { value: arbitrationCost }); + + await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + + const lookahead = await sortitionModule.rngLookahead(); + await sortitionModule.passPhase(); // Staking -> Generating + for (let index = 0; index < lookahead; index++) { + await network.provider.send("evm_mine"); + } + + const requestId = await vrfConsumer.lastRequestId(); // Needed as we emulate the vrfCoordinator manually + await vrfCoordinator.fulfillRandomWords(requestId, vrfConsumer.address); // The callback calls sortitionModule.passPhase(); // Generating -> Drawing await core.draw(0, 5000); diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 707048574..4f36bc5a0 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -75,7 +75,7 @@ describe("Integration tests", async () => { sortitionModule = (await ethers.getContract("SortitionModule")) as SortitionModule; }); - it("Resolves a dispute on the home chain with no appeal", async () => { + it("Resolves a dispute on the home chain with no appeal - Randomizer", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); const [, , relayer] = await ethers.getSigners(); @@ -165,10 +165,118 @@ describe("Integration tests", async () => { await mineBlocks(await sortitionModule.rngLookahead()); // Wait for finality expect(await sortitionModule.phase()).to.equal(Phase.generating); console.log("KC phase: %d", await sortitionModule.phase()); - // await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); - const reqId = await vrfConsumer.lastRequestId(); - await vrfCoordinator.fulfillRandomWords(reqId, vrfConsumer.address); - // await sortitionModule.passPhase(); // Generating -> Drawing + + await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); + await sortitionModule.passPhase(); // Generating -> Drawing + expect(await sortitionModule.phase()).to.equal(Phase.drawing); + console.log("KC phase: %d", await sortitionModule.phase()); + + const tx3 = await core.draw(0, 1000); + console.log("draw successful"); + const events3 = (await tx3.wait()).events; + + const roundInfo = await core.getRoundInfo(0, 0); + expect(roundInfo.drawnJurors).deep.equal([deployer, deployer, deployer]); + expect(roundInfo.tokensAtStakePerJuror).to.equal(ONE_HUNDRED_PNK.mul(2)); + expect(roundInfo.totalFeesForJurors).to.equal(arbitrationCost); + + expect((await core.disputes(0)).period).to.equal(Period.evidence); + + await core.passPeriod(0); + expect((await core.disputes(0)).period).to.equal(Period.vote); + await disputeKit.connect(await ethers.getSigner(deployer)).castVote(0, [0, 1, 2], 0, 0, ""); + await core.passPeriod(0); + + await network.provider.send("evm_increaseTime", [100]); // Wait for the appeal period + await network.provider.send("evm_mine"); + + await core.passPeriod(0); + expect((await core.disputes(0)).period).to.equal(Period.execution); + expect(await core.execute(0, 0, 1000)).to.emit(core, "TokenAndETHShift"); + + const tx4 = await core.executeRuling(0, { gasLimit: 10000000, gasPrice: 5000000000 }); + console.log("Ruling executed on KlerosCore"); + expect(tx4).to.emit(core, "Ruling").withArgs(homeGateway.address, 0, 0); + expect(tx4).to.emit(arbitrable, "Ruling").withArgs(foreignGateway.address, 1, 0); // The ForeignGateway starts counting disputeID from 1. + }); + + it("Resolves a dispute on the home chain with no appeal - Chainlink VRF v2", async () => { + const arbitrationCost = ONE_TENTH_ETH.mul(3); + const [bridger, challenger, relayer] = await ethers.getSigners(); + const RNG_LOOKAHEAD = 20; + + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + + await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); + + await core.setStake(1, ONE_THOUSAND_PNK); + await core.getJurorBalance(deployer, 1).then((result) => { + expect(result.staked).to.equal(ONE_THOUSAND_PNK); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(1, ONE_HUNDRED_PNK.mul(5)); + await core.getJurorBalance(deployer, 1).then((result) => { + expect(result.staked).to.equal(ONE_HUNDRED_PNK.mul(5)); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(1, 0); + await core.getJurorBalance(deployer, 1).then((result) => { + expect(result.staked).to.equal(0); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + + await core.setStake(1, ONE_THOUSAND_PNK.mul(4)); + await core.getJurorBalance(deployer, 1).then((result) => { + expect(result.staked).to.equal(ONE_THOUSAND_PNK.mul(4)); + expect(result.locked).to.equal(0); + logJurorBalance(result); + }); + const tx = await arbitrable.createDispute(2, "0x00", 0, { + value: arbitrationCost, + }); + const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); + const [disputeId] = ethers.utils.defaultAbiCoder.decode(["uint"], `0x${trace.returnValue}`); // get returned value from createDispute() + console.log("Dispute Created"); + expect(tx).to.emit(foreignGateway, "DisputeCreation"); + expect(tx).to.emit(foreignGateway, "OutgoingDispute"); + console.log(`disputeId: ${disputeId}`); + + const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1); + const disputeHash = ethers.utils.solidityKeccak256( + ["uint", "bytes", "bytes", "uint", "uint", "bytes", "address"], + [31337, lastBlock.hash, ethers.utils.toUtf8Bytes("createDispute"), disputeId, 2, "0x00", arbitrable.address] + ); + + const events = (await tx.wait()).events; + + // Relayer tx + const tx2 = await homeGateway + .connect(relayer) + .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { + value: arbitrationCost, + }); + expect(tx2).to.emit(homeGateway, "Dispute"); + const events2 = (await tx2.wait()).events; + + await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + + expect(await sortitionModule.phase()).to.equal(Phase.staking); + expect(await sortitionModule.disputesWithoutJurors()).to.equal(1); + console.log("KC phase: %d", await sortitionModule.phase()); + + await sortitionModule.passPhase(); // Staking -> Generating + await mineBlocks(await sortitionModule.rngLookahead()); // Wait for finality + expect(await sortitionModule.phase()).to.equal(Phase.generating); + console.log("KC phase: %d", await sortitionModule.phase()); + + const requestId = await vrfConsumer.lastRequestId(); // Needed as we emulate the vrfCoordinator manually + await vrfCoordinator.fulfillRandomWords(requestId, vrfConsumer.address); // The callback calls sortitionModule.passPhase(); // Generating -> Drawing expect(await sortitionModule.phase()).to.equal(Phase.drawing); console.log("KC phase: %d", await sortitionModule.phase()); From eb49a063b192ccf811d0348e0fb309cdd7ecbab8 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 08:07:52 +0100 Subject: [PATCH 17/30] feat(contracts): create subscription in constructor --- contracts/src/rng/VRFSubscriptionManagerV2.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/src/rng/VRFSubscriptionManagerV2.sol b/contracts/src/rng/VRFSubscriptionManagerV2.sol index f96d31dde..9c400d345 100644 --- a/contracts/src/rng/VRFSubscriptionManagerV2.sol +++ b/contracts/src/rng/VRFSubscriptionManagerV2.sol @@ -72,6 +72,7 @@ contract VRFSubscriptionManagerV2 { vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); linkToken = LinkTokenInterface(_linkToken); governor = _governor; + createNewSubscription(); } // ************************************* // @@ -112,7 +113,7 @@ contract VRFSubscriptionManagerV2 { /** * @dev Creates a new subscription, overriding the previous one to be manageable by the contract. */ - function createNewSubscription() external onlyByGovernor { + function createNewSubscription() public onlyByGovernor { subscriptionId = vrfCoordinator.createSubscription(); } From 1dea1360259ab4bbb921c3a81131856fe1c3563c Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 08:10:27 +0100 Subject: [PATCH 18/30] chore: remove unnecessary new subscription tx --- contracts/deploy/00-home-chain-arbitration.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index baf31c3be..5dce62ecf 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -183,7 +183,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const vrfSubscriptionManagerContract = (await hre.ethers.getContract( "VRFSubscriptionManagerV2Mock" )) as VRFSubscriptionManagerV2Mock; - await vrfSubscriptionManagerContract.createNewSubscription(); await vrfSubscriptionManagerContract.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK const subId = await vrfSubscriptionManagerContract.subscriptionId(); const vrfConsumer = await deploy("VRFConsumerV2", { From 841c73a771969bc5583c74c4cd2d3836da4f87f6 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 08:11:07 +0100 Subject: [PATCH 19/30] refactor: change variable name --- contracts/deploy/00-home-chain-arbitration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 5dce62ecf..02ecc3bb6 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -184,7 +184,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) "VRFSubscriptionManagerV2Mock" )) as VRFSubscriptionManagerV2Mock; await vrfSubscriptionManagerContract.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK - const subId = await vrfSubscriptionManagerContract.subscriptionId(); + const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId(); const vrfConsumer = await deploy("VRFConsumerV2", { from: deployer, args: [ @@ -192,7 +192,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) vrfCoordinator, sortitionModule.address, keyHash, - subId, + subscriptionId, requestConfirmations, callbackGasLimit, numWords, From ff1a5fd146fa21904c54a43fb5c6bad8af18014c Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 08:23:05 +0100 Subject: [PATCH 20/30] feat(deploy): add deployment for public networks --- contracts/deploy/00-home-chain-arbitration.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index 02ecc3bb6..b4bf5e207 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -4,7 +4,7 @@ import { BigNumber } from "ethers"; import getContractAddress from "./utils/getContractAddress"; import { deployUpgradable } from "./utils/deployUpgradable"; import { HomeChains, isSkipped, isDevnet } from "./utils"; -import { VRFSubscriptionManagerV2Mock, SortitionModule } from "../typechain-types"; +import { VRFSubscriptionManagerV2, VRFSubscriptionManagerV2Mock } from "../typechain-types"; const pnkByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x330bD769382cFc6d50175903434CCC8D206DCAE5"], @@ -177,8 +177,8 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) : AddressZero; // Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node + // The Sortition Module rng source is not changed to the VRF Consumer. if (vrfSubscriptionManager) { - // The Sortition Module is not changed to the VRF Consumer, it must be done in the test. if (chainId === HomeChains.HARDHAT) { const vrfSubscriptionManagerContract = (await hre.ethers.getContract( "VRFSubscriptionManagerV2Mock" @@ -201,6 +201,26 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }); await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); } + } else { + const vrfSubscriptionManagerContract = (await hre.ethers.getContract( + "VRFSubscriptionManagerV2" + )) as VRFSubscriptionManagerV2; + const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId(); + const vrfConsumer = await deploy("VRFConsumerV2", { + from: deployer, + args: [ + deployer, + vrfCoordinator, + sortitionModule.address, + keyHash, + subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords, + ], + log: true, + }); + await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); } }; From d9384b6c5dece80013103dd53e5322741cb0ce2e Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 08:40:18 +0100 Subject: [PATCH 21/30] fix(contracts): update mock to also create subscription in constructor --- contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol index 96108a8af..616df739e 100644 --- a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol +++ b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol @@ -60,6 +60,7 @@ contract VRFSubscriptionManagerV2Mock { constructor(address _governor, address _vrfCoordinator) { vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); governor = _governor; + createNewSubscription(); } // ************************************* // @@ -81,7 +82,7 @@ contract VRFSubscriptionManagerV2Mock { /** * @dev Creates a new subscription, overriding the previous one to be manageable by the contract. */ - function createNewSubscription() external onlyByGovernor { + function createNewSubscription() public onlyByGovernor { subscriptionId = vrfCoordinator.createSubscription(); } From 55987a825345006b72687c0cad38b868d7b250b3 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 10:26:55 +0100 Subject: [PATCH 22/30] feat(deploy): add task to credit LINK from deployer to VRF subscription --- contracts/hardhat.config.ts | 1 + contracts/scripts/creditLink.ts | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 contracts/scripts/creditLink.ts diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index fb3dd48ce..80d178ac2 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -14,6 +14,7 @@ import "hardhat-docgen"; import "hardhat-contract-sizer"; import "hardhat-tracer"; require("./scripts/simulations/tasks"); +require("./scripts/creditLink"); dotenv.config(); diff --git a/contracts/scripts/creditLink.ts b/contracts/scripts/creditLink.ts new file mode 100644 index 000000000..be123b140 --- /dev/null +++ b/contracts/scripts/creditLink.ts @@ -0,0 +1,56 @@ +import { deployments, getNamedAccounts, getChainId, ethers } from "hardhat"; +import { LinkTokenInterface, VRFSubscriptionManagerV2 } from "../typechain-types"; +import { task, types } from "hardhat/config"; +import { BigNumber } from "ethers"; + +enum HomeChains { + ARBITRUM_ONE = 42161, + ARBITRUM_GOERLI = 421613, +} + +// https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum +const linkByChain = new Map([ + [HomeChains.ARBITRUM_ONE, "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"], + [HomeChains.ARBITRUM_GOERLI, "0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28"], +]); + +const ONE_LINK_WEI = BigNumber.from(10).pow(18); + +task("credit-link", "Credit LINK tokens to the current Chainlink VRF Subscription") + .addParam("amount", "(in normal values, not wei!) The amount of LINK tokens to credit") + .setAction(async (taskArgs, hre) => { + const { deployments, getNamedAccounts, getChainId, ethers } = hre; + const { AddressZero } = ethers.constants; + const deployer = (await getNamedAccounts()).deployer ?? AddressZero; + + const chainId = Number(await getChainId()); + if (!HomeChains[chainId]) { + console.error(`Aborting: script is not compatible with ${chainId}`); + return; + } else { + console.log("Crediting %d LINK to %s with deployer %s", HomeChains[chainId], deployer); + } + + const { amount } = taskArgs; + const amountInWei = BigNumber.from(amount).mul(ONE_LINK_WEI); + + // Retrieve LINK token contract from artifact to interact with it + const linkTokenAddress = linkByChain.get(Number(await getChainId())) ?? AddressZero; + const linkTokenArtifact = await hre.artifacts.readArtifact("LinkTokenInterface"); + const linkToken = (await ethers.getContractAtFromArtifact( + linkTokenArtifact, + linkTokenAddress + )) as LinkTokenInterface; + + const vrfSubscriptionManagerDeployment = await deployments.get("VRFSubscriptionManagerV2"); + const vrfSubscriptionManager = (await ethers.getContractAt( + "VRFSubscriptionManagerV2", + vrfSubscriptionManagerDeployment.address + )) as VRFSubscriptionManagerV2; + + // Transfer LINK from deployer to the Subscription Manager + await linkToken.transfer(vrfSubscriptionManager.address, amountInWei); + + // // Fund the subscription, sending `amount` LINK tokens + await vrfSubscriptionManager.topUpSubscription(amountInWei); + }); From 13c7b7d426cc5a6e4a9f493bea750c9e7c10cfe8 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 10:36:33 +0100 Subject: [PATCH 23/30] refactor: change keyHash arbitrary address to 0 --- contracts/deploy/00-home-chain-arbitration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index b4bf5e207..b5470207b 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -29,7 +29,7 @@ const linkByChain = new Map([ const keyHashByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63"], // 30 gwei key Hash [HomeChains.ARBITRUM_GOERLI, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], - [HomeChains.HARDHAT, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], // arbitrary value + [HomeChains.HARDHAT, "0x0000000000000000000000000000000000000000000000000000000000000000"], // arbitrary value ]); // https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet From e8469179fa43a76c35e8da8706cc01d7c0fdd65d Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 10:37:12 +0100 Subject: [PATCH 24/30] chore: update rng to last chainlink deploy version --- contracts/deploy/00-rng.ts | 53 ++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts index d8bb2ee1b..70e9c9f56 100644 --- a/contracts/deploy/00-rng.ts +++ b/contracts/deploy/00-rng.ts @@ -1,7 +1,7 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { BigNumber } from "ethers"; import { DeployFunction } from "hardhat-deploy/types"; -import { SortitionModule, VRFSubscriptionManagerV2Mock } from "../typechain-types"; +import { SortitionModule, VRFSubscriptionManagerV2Mock, VRFSubscriptionManagerV2 } from "../typechain-types"; import { HomeChains, isSkipped } from "./utils"; import { deployUpgradable } from "./utils/deployUpgradable"; @@ -10,22 +10,26 @@ const pnkByChain = new Map([ [HomeChains.ARBITRUM_GOERLI, "0x3483FA1b87792cd5BE4100822C4eCEC8D3E531ee"], ]); +// https://randomizer.ai/docs#addresses const randomizerByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x5b8bB80f2d72D0C85caB8fB169e8170A05C94bAF"], [HomeChains.ARBITRUM_GOERLI, "0x923096Da90a3b60eb7E12723fA2E1547BA9236Bc"], ]); +// https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum const linkByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"], [HomeChains.ARBITRUM_GOERLI, "0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28"], ]); +// https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet const keyHashByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63"], // 30 gwei key Hash [HomeChains.ARBITRUM_GOERLI, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], - [HomeChains.HARDHAT, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], + [HomeChains.HARDHAT, "0x0000000000000000000000000000000000000000000000000000000000000000"], ]); +// https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet const vrfCoordinatorByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x41034678D6C633D8a95c75e1138A360a28bA15d1"], [HomeChains.ARBITRUM_GOERLI, "0x6D80646bEAdd07cE68cab36c27c626790bBcf17f"], @@ -85,13 +89,14 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const sortitionModule = (await hre.ethers.getContract("SortitionModule")) as SortitionModule; await sortitionModule.changeRandomNumberGenerator(rng.address, RNG_LOOKAHEAD); - const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; + const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; // LINK not needed on hardhat local node const keyHash = keyHashByChain.get(Number(await getChainId())) ?? AddressZero; - const requestConfirmations = 3; - const callbackGasLimit = 100000; + const requestConfirmations = 3; // Paramater to be fixed, range [1 ; 200] on Arbitrum + const callbackGasLimit = 100000; // Parameter to be fixed, 50000 on RandomizerRNG but no external call to sortitionModule.passPhase() in the callback const numWords = 1; const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero; - const vrfSubscriptionManagerDeploy = vrfCoordinator + // Deploy the VRF Subscription Manager contract on Arbitrum, a mock contract on Hardhat node or nothing on other networks. + const vrfSubscriptionManager = vrfCoordinator ? chainId === HomeChains.HARDHAT ? await deploy("VRFSubscriptionManagerV2Mock", { from: deployer, @@ -105,14 +110,15 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }) : AddressZero; - if (vrfSubscriptionManagerDeploy) { + // Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node + // The Sortition Module rng source is not changed to the VRF Consumer. + if (vrfSubscriptionManager) { if (chainId === HomeChains.HARDHAT) { - const vrfSubscriptionManager = (await hre.ethers.getContract( + const vrfSubscriptionManagerContract = (await hre.ethers.getContract( "VRFSubscriptionManagerV2Mock" )) as VRFSubscriptionManagerV2Mock; - await vrfSubscriptionManager.createNewSubscription(); - await vrfSubscriptionManager.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK - const subId = await vrfSubscriptionManager.subscriptionId(); + await vrfSubscriptionManagerContract.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK + const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId(); const vrfConsumer = await deploy("VRFConsumerV2", { from: deployer, args: [ @@ -120,16 +126,35 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) vrfCoordinator, sortitionModule.address, keyHash, - subId, + subscriptionId, requestConfirmations, callbackGasLimit, numWords, ], log: true, }); - await vrfSubscriptionManager.addConsumer(vrfConsumer.address); - await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); } + } else { + const vrfSubscriptionManagerContract = (await hre.ethers.getContract( + "VRFSubscriptionManagerV2" + )) as VRFSubscriptionManagerV2; + const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId(); + const vrfConsumer = await deploy("VRFConsumerV2", { + from: deployer, + args: [ + deployer, + vrfCoordinator, + sortitionModule.address, + keyHash, + subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords, + ], + log: true, + }); + await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); } }; From 0ca6b82ff39ddf7ba10253c0b4f7933a4c5e8abf Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 13:46:17 +0100 Subject: [PATCH 25/30] chore: clean unused imports --- contracts/scripts/creditLink.ts | 3 +-- contracts/test/integration/index.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/scripts/creditLink.ts b/contracts/scripts/creditLink.ts index be123b140..ed0d5177e 100644 --- a/contracts/scripts/creditLink.ts +++ b/contracts/scripts/creditLink.ts @@ -1,6 +1,5 @@ -import { deployments, getNamedAccounts, getChainId, ethers } from "hardhat"; import { LinkTokenInterface, VRFSubscriptionManagerV2 } from "../typechain-types"; -import { task, types } from "hardhat/config"; +import { task } from "hardhat/config"; import { BigNumber } from "ethers"; enum HomeChains { diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 4f36bc5a0..542b2fdd4 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -14,7 +14,6 @@ import { RandomizerMock, SortitionModule, VRFConsumerV2, - VRFSubscriptionManagerV2Mock, VRFCoordinatorV2Mock, } from "../../typechain-types"; From 1bf82cbd46e91a958a635cf1f68f579776626a7f Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 13:49:08 +0100 Subject: [PATCH 26/30] refactor: remove nested ternary operator --- contracts/deploy/00-home-chain-arbitration.ts | 32 +++++++++++-------- contracts/deploy/00-rng.ts | 32 +++++++++++-------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index b5470207b..f95bdde01 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -1,5 +1,5 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; +import { DeployFunction, DeployResult } from "hardhat-deploy/types"; import { BigNumber } from "ethers"; import getContractAddress from "./utils/getContractAddress"; import { deployUpgradable } from "./utils/deployUpgradable"; @@ -162,19 +162,23 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const numWords = 1; const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero; // Deploy the VRF Subscription Manager contract on Arbitrum, a mock contract on Hardhat node or nothing on other networks. - const vrfSubscriptionManager = vrfCoordinator - ? chainId === HomeChains.HARDHAT - ? await deploy("VRFSubscriptionManagerV2Mock", { - from: deployer, - args: [deployer, vrfCoordinator], - log: true, - }) - : await deploy("VRFSubscriptionManagerV2", { - from: deployer, - args: [deployer, vrfCoordinator, link], - log: true, - }) - : AddressZero; + let vrfSubscriptionManager: DeployResult | string; + if (vrfCoordinator) { + vrfSubscriptionManager = + chainId === HomeChains.HARDHAT + ? await deploy("VRFSubscriptionManagerV2Mock", { + from: deployer, + args: [deployer, vrfCoordinator], + log: true, + }) + : await deploy("VRFSubscriptionManagerV2", { + from: deployer, + args: [deployer, vrfCoordinator, link], + log: true, + }); + } else { + vrfSubscriptionManager = AddressZero; + } // Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node // The Sortition Module rng source is not changed to the VRF Consumer. diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts index 70e9c9f56..fd8f576b5 100644 --- a/contracts/deploy/00-rng.ts +++ b/contracts/deploy/00-rng.ts @@ -1,6 +1,6 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { BigNumber } from "ethers"; -import { DeployFunction } from "hardhat-deploy/types"; +import { DeployFunction, DeployResult } from "hardhat-deploy/types"; import { SortitionModule, VRFSubscriptionManagerV2Mock, VRFSubscriptionManagerV2 } from "../typechain-types"; import { HomeChains, isSkipped } from "./utils"; import { deployUpgradable } from "./utils/deployUpgradable"; @@ -96,19 +96,23 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const numWords = 1; const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero; // Deploy the VRF Subscription Manager contract on Arbitrum, a mock contract on Hardhat node or nothing on other networks. - const vrfSubscriptionManager = vrfCoordinator - ? chainId === HomeChains.HARDHAT - ? await deploy("VRFSubscriptionManagerV2Mock", { - from: deployer, - args: [deployer, vrfCoordinator], - log: true, - }) - : await deploy("VRFSubscriptionManagerV2", { - from: deployer, - args: [deployer, vrfCoordinator, link], - log: true, - }) - : AddressZero; + let vrfSubscriptionManager: DeployResult | string; + if (vrfCoordinator) { + vrfSubscriptionManager = + chainId === HomeChains.HARDHAT + ? await deploy("VRFSubscriptionManagerV2Mock", { + from: deployer, + args: [deployer, vrfCoordinator], + log: true, + }) + : await deploy("VRFSubscriptionManagerV2", { + from: deployer, + args: [deployer, vrfCoordinator, link], + log: true, + }); + } else { + vrfSubscriptionManager = AddressZero; + } // Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node // The Sortition Module rng source is not changed to the VRF Consumer. From 9ce3f4b0bd0c31d604b2317e259194fe446d4110 Mon Sep 17 00:00:00 2001 From: zmalatrax Date: Wed, 21 Jun 2023 14:29:02 +0100 Subject: [PATCH 27/30] refactor: restore 00-rng to its initial state --- contracts/deploy/00-rng.ts | 107 +------------------------------------ 1 file changed, 2 insertions(+), 105 deletions(-) diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts index fd8f576b5..9d3716e1d 100644 --- a/contracts/deploy/00-rng.ts +++ b/contracts/deploy/00-rng.ts @@ -1,7 +1,6 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { BigNumber } from "ethers"; -import { DeployFunction, DeployResult } from "hardhat-deploy/types"; -import { SortitionModule, VRFSubscriptionManagerV2Mock, VRFSubscriptionManagerV2 } from "../typechain-types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { SortitionModule } from "../typechain-types"; import { HomeChains, isSkipped } from "./utils"; import { deployUpgradable } from "./utils/deployUpgradable"; @@ -10,31 +9,11 @@ const pnkByChain = new Map([ [HomeChains.ARBITRUM_GOERLI, "0x3483FA1b87792cd5BE4100822C4eCEC8D3E531ee"], ]); -// https://randomizer.ai/docs#addresses const randomizerByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x5b8bB80f2d72D0C85caB8fB169e8170A05C94bAF"], [HomeChains.ARBITRUM_GOERLI, "0x923096Da90a3b60eb7E12723fA2E1547BA9236Bc"], ]); -// https://docs.chain.link/resources/link-token-contracts?parent=vrf#arbitrum -const linkByChain = new Map([ - [HomeChains.ARBITRUM_ONE, "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"], - [HomeChains.ARBITRUM_GOERLI, "0xd14838A68E8AFBAdE5efb411d5871ea0011AFd28"], -]); - -// https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet -const keyHashByChain = new Map([ - [HomeChains.ARBITRUM_ONE, "0x72d2b016bb5b62912afea355ebf33b91319f828738b111b723b78696b9847b63"], // 30 gwei key Hash - [HomeChains.ARBITRUM_GOERLI, "0x83d1b6e3388bed3d76426974512bb0d270e9542a765cd667242ea26c0cc0b730"], - [HomeChains.HARDHAT, "0x0000000000000000000000000000000000000000000000000000000000000000"], -]); - -// https://docs.chain.link/vrf/v2/subscription/supported-networks#arbitrum-mainnet -const vrfCoordinatorByChain = new Map([ - [HomeChains.ARBITRUM_ONE, "0x41034678D6C633D8a95c75e1138A360a28bA15d1"], - [HomeChains.ARBITRUM_GOERLI, "0x6D80646bEAdd07cE68cab36c27c626790bBcf17f"], -]); - const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; const { deploy, execute } = deployments; @@ -66,16 +45,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }) ).address ); - vrfCoordinatorByChain.set( - HomeChains.HARDHAT, - ( - await deploy("VRFCoordinatorV2Mock", { - from: deployer, - args: [BigNumber.from(10).pow(18), 1000000000], // base_fee: 1 LINK, gas_price_link: 0.000000001 LINK per gas, from chainlink mock scripts - log: true, - }) - ).address - ); } const randomizer = randomizerByChain.get(Number(await getChainId())) ?? AddressZero; @@ -88,78 +57,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const sortitionModule = (await hre.ethers.getContract("SortitionModule")) as SortitionModule; await sortitionModule.changeRandomNumberGenerator(rng.address, RNG_LOOKAHEAD); - - const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; // LINK not needed on hardhat local node - const keyHash = keyHashByChain.get(Number(await getChainId())) ?? AddressZero; - const requestConfirmations = 3; // Paramater to be fixed, range [1 ; 200] on Arbitrum - const callbackGasLimit = 100000; // Parameter to be fixed, 50000 on RandomizerRNG but no external call to sortitionModule.passPhase() in the callback - const numWords = 1; - const vrfCoordinator = vrfCoordinatorByChain.get(Number(await getChainId())) ?? AddressZero; - // Deploy the VRF Subscription Manager contract on Arbitrum, a mock contract on Hardhat node or nothing on other networks. - let vrfSubscriptionManager: DeployResult | string; - if (vrfCoordinator) { - vrfSubscriptionManager = - chainId === HomeChains.HARDHAT - ? await deploy("VRFSubscriptionManagerV2Mock", { - from: deployer, - args: [deployer, vrfCoordinator], - log: true, - }) - : await deploy("VRFSubscriptionManagerV2", { - from: deployer, - args: [deployer, vrfCoordinator, link], - log: true, - }); - } else { - vrfSubscriptionManager = AddressZero; - } - - // Execute the setup transactions for using VRF and deploy the Consumer contract on Hardhat node - // The Sortition Module rng source is not changed to the VRF Consumer. - if (vrfSubscriptionManager) { - if (chainId === HomeChains.HARDHAT) { - const vrfSubscriptionManagerContract = (await hre.ethers.getContract( - "VRFSubscriptionManagerV2Mock" - )) as VRFSubscriptionManagerV2Mock; - await vrfSubscriptionManagerContract.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK - const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId(); - const vrfConsumer = await deploy("VRFConsumerV2", { - from: deployer, - args: [ - deployer, - vrfCoordinator, - sortitionModule.address, - keyHash, - subscriptionId, - requestConfirmations, - callbackGasLimit, - numWords, - ], - log: true, - }); - await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); - } - } else { - const vrfSubscriptionManagerContract = (await hre.ethers.getContract( - "VRFSubscriptionManagerV2" - )) as VRFSubscriptionManagerV2; - const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId(); - const vrfConsumer = await deploy("VRFConsumerV2", { - from: deployer, - args: [ - deployer, - vrfCoordinator, - sortitionModule.address, - keyHash, - subscriptionId, - requestConfirmations, - callbackGasLimit, - numWords, - ], - log: true, - }); - await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); - } }; deployArbitration.tags = ["RNG"]; From acfcd1b26695b13b7df350bba6855cd4833d4841 Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Mon, 30 Oct 2023 00:13:16 +1000 Subject: [PATCH 28/30] fix(RNG): test fixes --- contracts/deploy/00-home-chain-arbitration.ts | 5 -- contracts/test/arbitration/draw.ts | 19 ++++-- contracts/test/arbitration/unstake.ts | 2 +- contracts/test/integration/index.ts | 61 +++++++++++++------ 4 files changed, 57 insertions(+), 30 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index f95bdde01..c68a4a102 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -149,11 +149,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", pnk, 12225583, 12); await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", dai, 60327783, 11); await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", weth, 1, 1); - await deploy("DisputeResolver", { - from: deployer, - args: [klerosCore.address], - log: true, - }); const link = linkByChain.get(Number(await getChainId())) ?? AddressZero; // LINK not needed on hardhat local node const keyHash = keyHashByChain.get(Number(await getChainId())) ?? AddressZero; diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index d1600858c..db41e4338 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -441,7 +441,7 @@ describe("Draw Benchmark", async () => { } // Create a dispute - const tx = await arbitrable.createDispute(2, "0x00", 0, { + const tx = await arbitrable.functions["createDispute(string)"]("RNG test", { value: arbitrationCost, }); const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); @@ -451,9 +451,20 @@ describe("Draw Benchmark", async () => { // Relayer tx const tx2 = await homeGateway .connect(await ethers.getSigner(relayer)) - .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { - value: arbitrationCost, - }); + .functions["relayCreateDispute((bytes32,uint256,address,uint256,uint256,uint256,string,uint256,bytes))"]( + { + foreignBlockHash: lastBlock.hash, + foreignChainID: 31337, + foreignArbitrable: arbitrable.address, + foreignDisputeID: disputeId, + externalDisputeID: ethers.utils.keccak256(ethers.utils.toUtf8Bytes("RNG test")), + templateId: 0, + templateUri: "", + choices: 2, + extraData: `0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003`, // General Court, 3 jurors + }, + { value: arbitrationCost } + ); await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); diff --git a/contracts/test/arbitration/unstake.ts b/contracts/test/arbitration/unstake.ts index 818488710..4f22a133a 100644 --- a/contracts/test/arbitration/unstake.ts +++ b/contracts/test/arbitration/unstake.ts @@ -103,7 +103,7 @@ describe("Unstake juror", async () => { expect(await core.getJurorCourtIDs(deployer)).to.be.deep.equal([BigNumber.from("1"), BigNumber.from("2")]); - await core.createDispute(2, extraData, { value: arbitrationCost }); + await core.functions["createDispute(uint256,bytes)"](2, extraData, { value: arbitrationCost }); await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 542b2fdd4..6f23cc777 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -176,8 +176,9 @@ describe("Integration tests", async () => { const roundInfo = await core.getRoundInfo(0, 0); expect(roundInfo.drawnJurors).deep.equal([deployer, deployer, deployer]); - expect(roundInfo.tokensAtStakePerJuror).to.equal(ONE_HUNDRED_PNK.mul(2)); + expect(roundInfo.pnkAtStakePerJuror).to.equal(ONE_HUNDRED_PNK.mul(2)); expect(roundInfo.totalFeesForJurors).to.equal(arbitrationCost); + expect(roundInfo.feeToken).to.equal(ethers.constants.AddressZero); expect((await core.disputes(0)).period).to.equal(Period.evidence); @@ -210,55 +211,75 @@ describe("Integration tests", async () => { await core.setStake(1, ONE_THOUSAND_PNK); await core.getJurorBalance(deployer, 1).then((result) => { - expect(result.staked).to.equal(ONE_THOUSAND_PNK); - expect(result.locked).to.equal(0); + expect(result.totalStaked).to.equal(ONE_THOUSAND_PNK); + expect(result.totalLocked).to.equal(0); logJurorBalance(result); }); await core.setStake(1, ONE_HUNDRED_PNK.mul(5)); await core.getJurorBalance(deployer, 1).then((result) => { - expect(result.staked).to.equal(ONE_HUNDRED_PNK.mul(5)); - expect(result.locked).to.equal(0); + expect(result.totalStaked).to.equal(ONE_HUNDRED_PNK.mul(5)); + expect(result.totalLocked).to.equal(0); logJurorBalance(result); }); await core.setStake(1, 0); await core.getJurorBalance(deployer, 1).then((result) => { - expect(result.staked).to.equal(0); - expect(result.locked).to.equal(0); + expect(result.totalStaked).to.equal(0); + expect(result.totalLocked).to.equal(0); logJurorBalance(result); }); await core.setStake(1, ONE_THOUSAND_PNK.mul(4)); await core.getJurorBalance(deployer, 1).then((result) => { - expect(result.staked).to.equal(ONE_THOUSAND_PNK.mul(4)); - expect(result.locked).to.equal(0); + expect(result.totalStaked).to.equal(ONE_THOUSAND_PNK.mul(4)); + expect(result.totalLocked).to.equal(0); logJurorBalance(result); }); - const tx = await arbitrable.createDispute(2, "0x00", 0, { + const tx = await arbitrable.functions["createDispute(string)"]("RNG test", { value: arbitrationCost, }); const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); const [disputeId] = ethers.utils.defaultAbiCoder.decode(["uint"], `0x${trace.returnValue}`); // get returned value from createDispute() - console.log("Dispute Created"); - expect(tx).to.emit(foreignGateway, "DisputeCreation"); - expect(tx).to.emit(foreignGateway, "OutgoingDispute"); - console.log(`disputeId: ${disputeId}`); + console.log("Dispute Created with disputeId: %d", disputeId); + await expect(tx) + .to.emit(foreignGateway, "CrossChainDisputeOutgoing") + .withArgs(anyValue, arbitrable.address, 1, 2, "0x00"); + await expect(tx) + .to.emit(arbitrable, "DisputeRequest") + .withArgs( + foreignGateway.address, + 1, + BigNumber.from("100587076116875319099890440047601180158236049259177371049006183970829186180694"), + 0, + "" + ); const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1); const disputeHash = ethers.utils.solidityKeccak256( - ["uint", "bytes", "bytes", "uint", "uint", "bytes", "address"], - [31337, lastBlock.hash, ethers.utils.toUtf8Bytes("createDispute"), disputeId, 2, "0x00", arbitrable.address] + ["bytes", "bytes32", "uint256", "address", "uint256", "uint256", "bytes"], + [ethers.utils.toUtf8Bytes("createDispute"), lastBlock.hash, 31337, arbitrable.address, disputeId, 2, "0x00"] ); - const events = (await tx.wait()).events; + console.log("dispute hash: ", disputeHash); // Relayer tx const tx2 = await homeGateway .connect(relayer) - .relayCreateDispute(31337, lastBlock.hash, disputeId, 2, "0x00", arbitrable.address, { - value: arbitrationCost, - }); + .functions["relayCreateDispute((bytes32,uint256,address,uint256,uint256,uint256,string,uint256,bytes))"]( + { + foreignBlockHash: lastBlock.hash, + foreignChainID: 31337, + foreignArbitrable: arbitrable.address, + foreignDisputeID: disputeId, + externalDisputeID: ethers.utils.keccak256(ethers.utils.toUtf8Bytes("RNG test")), + templateId: 0, + templateUri: "", + choices: 2, + extraData: "0x00", + }, + { value: arbitrationCost } + ); expect(tx2).to.emit(homeGateway, "Dispute"); const events2 = (await tx2.wait()).events; From 042869332fa2a35521c303e067ce231465f08021 Mon Sep 17 00:00:00 2001 From: unknownunknown1 Date: Mon, 30 Oct 2023 03:10:35 +1000 Subject: [PATCH 29/30] feat(RNG): add fallback + upgradability --- contracts/deploy/00-home-chain-arbitration.ts | 12 ++- contracts/deploy/00-rng.ts | 3 +- contracts/deploy/upgrade-sortition-module.ts | 4 +- contracts/scripts/simulations/tasks.ts | 2 - contracts/src/arbitration/SortitionModule.sol | 44 +++++++---- contracts/src/rng/BlockhashRNG.sol | 2 + contracts/src/rng/IncrementalNG.sol | 2 + contracts/src/rng/RNG.sol | 2 + contracts/src/rng/RandomizerRNG.sol | 2 + contracts/src/rng/VRFConsumerBaseV2.sol | 8 +- contracts/src/rng/VRFConsumerV2.sol | 77 ++++++++++++++++--- contracts/test/arbitration/draw.ts | 15 +--- contracts/test/arbitration/unstake.ts | 11 +-- contracts/test/integration/index.ts | 5 +- 14 files changed, 123 insertions(+), 66 deletions(-) diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index c68a4a102..0d2dc283e 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -42,7 +42,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const { ethers, deployments, getNamedAccounts, getChainId } = hre; const { deploy, execute } = deployments; const { AddressZero } = hre.ethers.constants; - const RNG_LOOKAHEAD = 20; + const RNG_FALLBACK = 150; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -110,7 +110,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const maxFreezingTime = devnet ? 600 : 1800; const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.address, RNG_LOOKAHEAD], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.address, RNG_FALLBACK], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -184,7 +184,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) )) as VRFSubscriptionManagerV2Mock; await vrfSubscriptionManagerContract.topUpSubscription(BigNumber.from(10).pow(20)); // 100 LINK const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId(); - const vrfConsumer = await deploy("VRFConsumerV2", { + const vrfConsumer = await deployUpgradable(deployments, "VRFConsumerV2", { from: deployer, args: [ deployer, @@ -195,6 +195,8 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) requestConfirmations, callbackGasLimit, numWords, + AddressZero, + AddressZero, ], log: true, }); @@ -205,7 +207,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) "VRFSubscriptionManagerV2" )) as VRFSubscriptionManagerV2; const subscriptionId = await vrfSubscriptionManagerContract.subscriptionId(); - const vrfConsumer = await deploy("VRFConsumerV2", { + const vrfConsumer = await deployUpgradable(deployments, "VRFConsumerV2", { from: deployer, args: [ deployer, @@ -216,6 +218,8 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) requestConfirmations, callbackGasLimit, numWords, + AddressZero, + AddressZero, ], log: true, }); diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts index 9d3716e1d..022a06903 100644 --- a/contracts/deploy/00-rng.ts +++ b/contracts/deploy/00-rng.ts @@ -18,7 +18,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const { deployments, getNamedAccounts, getChainId } = hre; const { deploy, execute } = deployments; const { AddressZero } = hre.ethers.constants; - const RNG_LOOKAHEAD = 20; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -56,7 +55,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }); const sortitionModule = (await hre.ethers.getContract("SortitionModule")) as SortitionModule; - await sortitionModule.changeRandomNumberGenerator(rng.address, RNG_LOOKAHEAD); + await sortitionModule.changeRandomNumberGenerator(rng.address); }; deployArbitration.tags = ["RNG"]; diff --git a/contracts/deploy/upgrade-sortition-module.ts b/contracts/deploy/upgrade-sortition-module.ts index 64962a449..eca03127d 100644 --- a/contracts/deploy/upgrade-sortition-module.ts +++ b/contracts/deploy/upgrade-sortition-module.ts @@ -11,7 +11,7 @@ enum HomeChains { const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; - const RNG_LOOKAHEAD = 20; + const RNG_FALLBACK = 150; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -32,7 +32,7 @@ const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeE 1800, // minStakingTime 1800, // maxFreezingTime rng.address, - RNG_LOOKAHEAD, + RNG_FALLBACK, ], }); } catch (err) { diff --git a/contracts/scripts/simulations/tasks.ts b/contracts/scripts/simulations/tasks.ts index b41899664..f1e2b2627 100644 --- a/contracts/scripts/simulations/tasks.ts +++ b/contracts/scripts/simulations/tasks.ts @@ -406,8 +406,6 @@ task("simulate:to-freezing-and-generating-phase", "Pass phase from 'staking' to if (isNetworkLocal(hre)) { const { sortition, randomizerMock, randomizerRng } = await getContracts(hre); const { wallet } = await getWallet(hre, walletindex); - const numberOfBlocksToMine = Number(await sortition.rngLookahead()); - await mineBlocks(numberOfBlocksToMine, hre.network); await randomizerMock.connect(wallet).relay(randomizerRng.address, 0, utils.randomBytes(32)); } }); diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index 67c787aff..5ca57fd9c 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -58,7 +58,7 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable { uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. RNG public rng; // The random number generator. uint256 public randomNumber; // Random number returned by RNG. - uint256 public rngLookahead; // Minimal block distance between requesting and obtaining a random number. + uint256 public rngFallbackTimeout; // Time after which RNG fallback will be used if no random number was received. uint256 public delayedStakeWriteIndex; // The index of the last `delayedStake` item that was written to the array. 0 index is skipped. uint256 public delayedStakeReadIndex; // The index of the next `delayedStake` item that should be processed. Starts at 1 because 0 index is skipped. mapping(bytes32 => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys. @@ -92,14 +92,14 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable { /// @param _minStakingTime Minimal time to stake /// @param _maxDrawingTime Time after which the drawing phase can be switched /// @param _rng The random number generator. - /// @param _rngLookahead Lookahead value for rng. + /// @param _rngFallbackTimeout RNG fallback timeout in seconds. function initialize( address _governor, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, RNG _rng, - uint256 _rngLookahead + uint256 _rngFallbackTimeout ) external reinitializer(1) { governor = _governor; core = _core; @@ -107,7 +107,7 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable { maxDrawingTime = _maxDrawingTime; lastPhaseChange = block.timestamp; rng = _rng; - rngLookahead = _rngLookahead; + rngFallbackTimeout = _rngFallbackTimeout; delayedStakeReadIndex = 1; } @@ -135,18 +135,22 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable { maxDrawingTime = _maxDrawingTime; } - /// @dev Changes the `_rng` and `_rngLookahead` storage variables. + /// @dev Changes the `_rng` storage variable. /// @param _rng The new value for the `RNGenerator` storage variable. - /// @param _rngLookahead The new value for the `rngLookahead` storage variable. - function changeRandomNumberGenerator(RNG _rng, uint256 _rngLookahead) external onlyByGovernor { + function changeRandomNumberGenerator(RNG _rng) external onlyByGovernor { rng = _rng; - rngLookahead = _rngLookahead; if (phase == Phase.generating) { - rng.requestRandomness(block.number + rngLookahead); + rng.requestRandomness(block.number); randomNumberRequestBlock = block.number; } } + /// @dev Changes the `rngFallbackTimeout` storage variable. + /// @param _rngFallbackTimeout The new value for the `rngFallbackTimeout` storage variable. + function changeRNGFallbackTimeout(uint256 _rngFallbackTimeout) external onlyByGovernor { + rngFallbackTimeout = _rngFallbackTimeout; + } + // ************************************* // // * State Modifiers * // // ************************************* // @@ -158,23 +162,31 @@ contract SortitionModule is ISortitionModule, UUPSProxiable, Initializable { "The minimum staking time has not passed yet." ); require(disputesWithoutJurors > 0, "There are no disputes that need jurors."); - rng.requestRandomness(block.number + rngLookahead); + rng.requestRandomness(block.number); randomNumberRequestBlock = block.number; phase = Phase.generating; + lastPhaseChange = block.timestamp; + emit NewPhase(phase); } else if (phase == Phase.generating) { - randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); - require(randomNumber != 0, "Random number is not ready yet"); - phase = Phase.drawing; + randomNumber = rng.receiveRandomness(randomNumberRequestBlock); + if (randomNumber == 0) { + if (block.number > randomNumberRequestBlock + rngFallbackTimeout) { + rng.receiveRandomnessFallback(block.number); + } + } else { + phase = Phase.drawing; + lastPhaseChange = block.timestamp; + emit NewPhase(phase); + } } else if (phase == Phase.drawing) { require( disputesWithoutJurors == 0 || block.timestamp - lastPhaseChange >= maxDrawingTime, "There are still disputes without jurors and the maximum drawing time has not passed yet." ); phase = Phase.staking; + lastPhaseChange = block.timestamp; + emit NewPhase(phase); } - - lastPhaseChange = block.timestamp; - emit NewPhase(phase); } /// @dev Create a sortition sum tree at the specified key. diff --git a/contracts/src/rng/BlockhashRNG.sol b/contracts/src/rng/BlockhashRNG.sol index 781fb3f7e..cba8984b4 100644 --- a/contracts/src/rng/BlockhashRNG.sol +++ b/contracts/src/rng/BlockhashRNG.sol @@ -41,4 +41,6 @@ contract BlockHashRNG is RNG { } randomNumbers[_block] = randomNumber; } + + function receiveRandomnessFallback(uint256 _block) external {} } diff --git a/contracts/src/rng/IncrementalNG.sol b/contracts/src/rng/IncrementalNG.sol index 6cede4dae..e5fe1a840 100644 --- a/contracts/src/rng/IncrementalNG.sol +++ b/contracts/src/rng/IncrementalNG.sol @@ -28,4 +28,6 @@ contract IncrementalNG is RNG { return number++; } } + + function receiveRandomnessFallback(uint256 _block) external {} } diff --git a/contracts/src/rng/RNG.sol b/contracts/src/rng/RNG.sol index ad9c597a3..9b0f5bfbb 100644 --- a/contracts/src/rng/RNG.sol +++ b/contracts/src/rng/RNG.sol @@ -11,4 +11,6 @@ interface RNG { /// @param _block Block the random number is linked to. /// @return randomNumber Random Number. If the number is not ready or has not been required 0 instead. function receiveRandomness(uint256 _block) external returns (uint256 randomNumber); + + function receiveRandomnessFallback(uint256 _block) external; } diff --git a/contracts/src/rng/RandomizerRNG.sol b/contracts/src/rng/RandomizerRNG.sol index 41dc072f7..e85ec86ee 100644 --- a/contracts/src/rng/RandomizerRNG.sol +++ b/contracts/src/rng/RandomizerRNG.sol @@ -99,6 +99,8 @@ contract RandomizerRNG is RNG, UUPSProxiable, Initializable { randomNumbers[_id] = uint256(_value); } + function receiveRandomnessFallback(uint256 _block) external {} + // ************************************* // // * Public Views * // // ************************************* // diff --git a/contracts/src/rng/VRFConsumerBaseV2.sol b/contracts/src/rng/VRFConsumerBaseV2.sol index c2974c13e..1a22aa066 100644 --- a/contracts/src/rng/VRFConsumerBaseV2.sol +++ b/contracts/src/rng/VRFConsumerBaseV2.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; +import "../proxy/Initializable.sol"; + /** **************************************************************************** * @notice Interface for contracts using VRF randomness * ***************************************************************************** @@ -94,14 +96,14 @@ pragma solidity ^0.8.4; * @dev responding to the request (however this is not enforced in the contract * @dev and so remains effective only in the case of unmodified oracle software). */ -abstract contract VRFConsumerBaseV2 { +abstract contract VRFConsumerBaseV2 is Initializable { error OnlyCoordinatorCanFulfill(address have, address want); - address private immutable vrfCoordinator; + address private vrfCoordinator; /** * @param _vrfCoordinator address of VRFCoordinator contract */ - constructor(address _vrfCoordinator) { + function vrfBase_init(address _vrfCoordinator) public onlyInitializing { vrfCoordinator = _vrfCoordinator; } diff --git a/contracts/src/rng/VRFConsumerV2.sol b/contracts/src/rng/VRFConsumerV2.sol index c2447be0d..f377c22ab 100644 --- a/contracts/src/rng/VRFConsumerV2.sol +++ b/contracts/src/rng/VRFConsumerV2.sol @@ -12,10 +12,13 @@ pragma solidity 0.8.18; import "./RNG.sol"; import "./VRFConsumerBaseV2.sol"; import "./interfaces/VRFCoordinatorV2Interface.sol"; +import "../proxy/UUPSProxiable.sol"; // Interface to call passPhase in the callback function interface ISortitionModule { function passPhase() external; + + function rngFallbackTimeout() external view returns (uint256); } /** @@ -28,7 +31,7 @@ interface ISortitionModule { * @dev For SECURITY CONSIDERATIONS, you might also have look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol * @dev For SECURITY CONSIDERATIONS, you might also have look to: https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol */ -contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { +contract VRFConsumerV2 is VRFConsumerBaseV2, RNG, UUPSProxiable { // ************************************* // // * Events * // // ************************************* // @@ -60,8 +63,11 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { uint32 public numWords; uint16 public requestConfirmations; uint256 public lastRequestId; + RNG public randomizerRNG; + RNG public blockhashRNG; mapping(uint256 => uint256) public requestsToRandomWords; // s_requests[requestId] = randomWord + mapping(uint256 => uint256) public requestToFallbackBlock; // Maps a Chainlink request ID with the number of the block where fallback RNG was triggered. // ************************************* // // * Function Modifiers * // @@ -77,9 +83,10 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { _; } - // ************************************* // - // * Constructor * // - // ************************************* // + /// @dev Constructor, initializing the implementation to reduce attack surface. + constructor() { + _disableInitializers(); + } /** * @dev Constructs the ChainlinkRNG contract. @@ -91,9 +98,11 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { * @param _requestConfirmations How many confirmations the Chainlink node should wait before responding. * @param _callbackGasLimit The limit for how much gas to use for the callback request to the contract's fulfillRandomWords() function. * @param _numWords How many random values to request. + * @param _randomizerRNG Address of Randomizer RNG contract. + * @param _blockhashRNG Adress of Blockhash RNG contract. * @dev https://docs.chain.link/vrf/v2/subscription/examples/get-a-random-number#analyzing-the-contract */ - constructor( + function initialize( address _governor, address _vrfCoordinator, address _sortitionModule, @@ -101,8 +110,11 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { uint64 _subscriptionId, uint16 _requestConfirmations, uint32 _callbackGasLimit, - uint32 _numWords - ) VRFConsumerBaseV2(_vrfCoordinator) { + uint32 _numWords, + RNG _randomizerRNG, + RNG _blockhashRNG + ) external reinitializer(1) { + vrfBase_init(_vrfCoordinator); governor = _governor; vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); sortitionModule = ISortitionModule(_sortitionModule); @@ -111,12 +123,20 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { requestConfirmations = _requestConfirmations; callbackGasLimit = _callbackGasLimit; numWords = _numWords; + randomizerRNG = _randomizerRNG; + blockhashRNG = _blockhashRNG; } // ************************************* // // * Governance * // // ************************************* // + /** + * @dev Access Control to perform implementation upgrades (UUPS Proxiable) + * @dev Only the governor can perform upgrades (`onlyByGovernor`) + */ + function _authorizeUpgrade(address) internal view override onlyByGovernor {} + /** * @dev Changes the `vrfCoordinator` storage variable. * @param _vrfCoordinator The new value for the `vrfCoordinator` storage variable. @@ -173,6 +193,22 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { numWords = _numWords; } + /** + * @dev Changes the `randomizerRNG` storage variable. + * @param _randomizerRNG The new value for the `randomizerRNG` storage variable. + */ + function changeRandomizerRNG(RNG _randomizerRNG) external onlyByGovernor { + randomizerRNG = _randomizerRNG; + } + + /** + * @dev Changes the `blockhashRNG` storage variable. + * @param _blockhashRNG The new value for the `blockhashRNG` storage variable. + */ + function changeBlockhashRNG(RNG _blockhashRNG) external onlyByGovernor { + blockhashRNG = _blockhashRNG; + } + // ************************************* // // * State Modifiers * // // ************************************* // @@ -201,7 +237,7 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { /** * @dev Callback function used by VRF Coordinator - * @dev Stores the random number given by the VRF Coordinator and calls passPhase function on SortitionModule. + * @dev Stores the random number given by the VRF Coordinator. * @param _requestId The same request Id initially returned by `vrfCoordinator.requestRandomWords` and stored in the `lastRequestId` storage variable. * @param _randomWords - array of random results from VRF Coordinator */ @@ -219,7 +255,28 @@ contract VRFConsumerV2 is VRFConsumerBaseV2, RNG { * @dev Get the random value associated to `lastRequestId` * @return randomNumber The random number. If the value is not ready or has not been required it returns 0. */ - function receiveRandomness(uint256 /* _block */) external view returns (uint256 randomNumber) { - randomNumber = requestsToRandomWords[lastRequestId]; + function receiveRandomness(uint256 /* _block */) external returns (uint256 randomNumber) { + if (requestsToRandomWords[lastRequestId] == 0) { + uint256 fallbackBlock = requestToFallbackBlock[lastRequestId]; + if (fallbackBlock != 0) { + if (block.number <= fallbackBlock + sortitionModule.rngFallbackTimeout()) { + randomNumber = randomizerRNG.receiveRandomness(0); + } else { + // We can use fallback block since it already gives enough distance from the block that first requested randomness. + // Note that if RNG fallback timeout is set higher or close than 256 blocks then blockhash will use the hash of the previous block instead. + randomNumber = blockhashRNG.receiveRandomness(fallbackBlock); + } + } + } else { + randomNumber = requestsToRandomWords[lastRequestId]; + } + } + + function receiveRandomnessFallback(uint256 _block) external onlyBySortitionModule { + if (requestToFallbackBlock[lastRequestId] == 0) { + requestToFallbackBlock[lastRequestId] = _block; + // Block number is irrelevant for Randomizer. + randomizerRNG.requestRandomness(0); + } } } diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index db41e4338..d4312a772 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -85,7 +85,7 @@ describe("Draw Benchmark", async () => { log: true, }); - await sortitionModule.changeRandomNumberGenerator(rng.address, 20); + await sortitionModule.changeRandomNumberGenerator(rng.address); // CourtId 2 = CHILD_COURT const minStake = BigNumber.from(10).pow(20).mul(3); // 300 PNK @@ -167,11 +167,6 @@ describe("Draw Benchmark", async () => { await network.provider.send("evm_mine"); await sortitionModule.passPhase(); // Staking -> Generating - const lookahead = await sortitionModule.rngLookahead(); - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } - await sortitionModule.passPhase(); // Generating -> Drawing await expectFromDraw(core.draw(0, 20, { gasLimit: 1000000 })); @@ -419,9 +414,8 @@ describe("Draw Benchmark", async () => { it("Draw Benchmark - Chainlink VRF v2", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); const [bridger] = await ethers.getSigners(); - const RNG_LOOKAHEAD = 20; - await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address); // Stake some jurors for (let i = 0; i < 16; i++) { @@ -470,11 +464,6 @@ describe("Draw Benchmark", async () => { await network.provider.send("evm_mine"); await sortitionModule.passPhase(); // Staking -> Generating - const lookahead = await sortitionModule.rngLookahead(); - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } - const requestId = await vrfConsumer.lastRequestId(); // Needed as we emulate the vrfCoordinator manually await vrfCoordinator.fulfillRandomWords(requestId, vrfConsumer.address); // The callback calls sortitionModule.passPhase(); // Generating -> Drawing diff --git a/contracts/test/arbitration/unstake.ts b/contracts/test/arbitration/unstake.ts index 4f22a133a..af8b2c5cc 100644 --- a/contracts/test/arbitration/unstake.ts +++ b/contracts/test/arbitration/unstake.ts @@ -65,11 +65,7 @@ describe("Unstake juror", async () => { await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); - const lookahead = await sortitionModule.rngLookahead(); await sortitionModule.passPhase(); // Staking -> Generating - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); await sortitionModule.passPhase(); // Generating -> Drawing @@ -91,9 +87,8 @@ describe("Unstake juror", async () => { it("Unstake inactive juror - Chainlink VRF v2", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); - const RNG_LOOKAHEAD = 20; - await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address); await core.createCourt(1, false, ONE_THOUSAND_PNK, 1000, ONE_TENTH_ETH, 3, [0, 0, 0, 0], 3, [1]); // Parent - general court, Classic dispute kit @@ -108,11 +103,7 @@ describe("Unstake juror", async () => { await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); - const lookahead = await sortitionModule.rngLookahead(); await sortitionModule.passPhase(); // Staking -> Generating - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } const requestId = await vrfConsumer.lastRequestId(); // Needed as we emulate the vrfCoordinator manually await vrfCoordinator.fulfillRandomWords(requestId, vrfConsumer.address); // The callback calls sortitionModule.passPhase(); // Generating -> Drawing diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 6f23cc777..5010d101d 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -161,7 +161,6 @@ describe("Integration tests", async () => { console.log("KC phase: %d", await sortitionModule.phase()); await sortitionModule.passPhase(); // Staking -> Generating - await mineBlocks(await sortitionModule.rngLookahead()); // Wait for finality expect(await sortitionModule.phase()).to.equal(Phase.generating); console.log("KC phase: %d", await sortitionModule.phase()); @@ -203,9 +202,8 @@ describe("Integration tests", async () => { it("Resolves a dispute on the home chain with no appeal - Chainlink VRF v2", async () => { const arbitrationCost = ONE_TENTH_ETH.mul(3); const [bridger, challenger, relayer] = await ethers.getSigners(); - const RNG_LOOKAHEAD = 20; - await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address, RNG_LOOKAHEAD); + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address); await pnk.approve(core.address, ONE_THOUSAND_PNK.mul(100)); @@ -291,7 +289,6 @@ describe("Integration tests", async () => { console.log("KC phase: %d", await sortitionModule.phase()); await sortitionModule.passPhase(); // Staking -> Generating - await mineBlocks(await sortitionModule.rngLookahead()); // Wait for finality expect(await sortitionModule.phase()).to.equal(Phase.generating); console.log("KC phase: %d", await sortitionModule.phase()); From 4b69a12487750766398258e84a3fcee4ce8fa861 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Tue, 12 Mar 2024 13:54:02 +0000 Subject: [PATCH 30/30] refactor: rename --- contracts/test/arbitration/{unstake.ts => staking.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/test/arbitration/{unstake.ts => staking.ts} (100%) diff --git a/contracts/test/arbitration/unstake.ts b/contracts/test/arbitration/staking.ts similarity index 100% rename from contracts/test/arbitration/unstake.ts rename to contracts/test/arbitration/staking.ts