diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index bf5b961bb..0d2dc283e 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -1,9 +1,10 @@ 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"; import { HomeChains, isSkipped, isDevnet } from "./utils"; +import { VRFSubscriptionManagerV2, VRFSubscriptionManagerV2Mock } from "../typechain-types"; const pnkByChain = new Map([ [HomeChains.ARBITRUM_ONE, "0x330bD769382cFc6d50175903434CCC8D206DCAE5"], @@ -18,12 +19,30 @@ 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, "0x0000000000000000000000000000000000000000000000000000000000000000"], // 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"], +]); 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; @@ -42,7 +61,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, @@ -80,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) @@ -119,6 +149,82 @@ 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); + + 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 deployUpgradable(deployments, "VRFConsumerV2", { + from: deployer, + args: [ + deployer, + vrfCoordinator, + sortitionModule.address, + keyHash, + subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords, + AddressZero, + AddressZero, + ], + 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 deployUpgradable(deployments, "VRFConsumerV2", { + from: deployer, + args: [ + deployer, + vrfCoordinator, + sortitionModule.address, + keyHash, + subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords, + AddressZero, + AddressZero, + ], + log: true, + }); + await vrfSubscriptionManagerContract.addConsumer(vrfConsumer.address); + } }; deployArbitration.tags = ["Arbitration"]; diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts index 30289b50d..022a06903 100644 --- a/contracts/deploy/00-rng.ts +++ b/contracts/deploy/00-rng.ts @@ -1,6 +1,6 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; -import { SortitionModule, RandomizerRNG } from "../typechain-types"; +import { SortitionModule } from "../typechain-types"; import { HomeChains, isSkipped } from "./utils"; import { deployUpgradable } from "./utils/deployUpgradable"; @@ -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/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..ed0d5177e --- /dev/null +++ b/contracts/scripts/creditLink.ts @@ -0,0 +1,55 @@ +import { LinkTokenInterface, VRFSubscriptionManagerV2 } from "../typechain-types"; +import { task } 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); + }); 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 new file mode 100644 index 000000000..1a22aa066 --- /dev/null +++ b/contracts/src/rng/VRFConsumerBaseV2.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "../proxy/Initializable.sol"; + +/** **************************************************************************** + * @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 is Initializable { + error OnlyCoordinatorCanFulfill(address have, address want); + address private vrfCoordinator; + + /** + * @param _vrfCoordinator address of VRFCoordinator contract + */ + function vrfBase_init(address _vrfCoordinator) public onlyInitializing { + 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..f377c22ab --- /dev/null +++ b/contracts/src/rng/VRFConsumerV2.sol @@ -0,0 +1,282 @@ +//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"; +import "../proxy/UUPSProxiable.sol"; + +// Interface to call passPhase in the callback function +interface ISortitionModule { + function passPhase() external; + + function rngFallbackTimeout() external view returns (uint256); +} + +/** + * @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, UUPSProxiable { + // ************************************* // + // * 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 public vrfCoordinator; + uint64 public subscriptionId; + uint32 public callbackGasLimit; + ISortitionModule public sortitionModule; + 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 * // + // ************************************* // + + modifier onlyBySortitionModule() { + require(msg.sender == address(sortitionModule), "Access not allowed: SortitionModule only"); + _; + } + + modifier onlyByGovernor() { + require(msg.sender == governor, "Access not allowed: Governor only"); + _; + } + + /// @dev Constructor, initializing the implementation to reduce attack surface. + constructor() { + _disableInitializers(); + } + + /** + * @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. + * @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 + */ + function initialize( + address _governor, + address _vrfCoordinator, + address _sortitionModule, + bytes32 _keyHash, + uint64 _subscriptionId, + uint16 _requestConfirmations, + uint32 _callbackGasLimit, + uint32 _numWords, + RNG _randomizerRNG, + RNG _blockhashRNG + ) external reinitializer(1) { + vrfBase_init(_vrfCoordinator); + governor = _governor; + vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator); + sortitionModule = ISortitionModule(_sortitionModule); + keyHash = _keyHash; + subscriptionId = _subscriptionId; + 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. + */ + 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; + } + + /** + * @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 * // + // ************************************* // + + /** + * @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. + * @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 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/src/rng/VRFSubscriptionManagerV2.sol b/contracts/src/rng/VRFSubscriptionManagerV2.sol new file mode 100644 index 000000000..9c400d345 --- /dev/null +++ b/contracts/src/rng/VRFSubscriptionManagerV2.sol @@ -0,0 +1,182 @@ +//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 public vrfCoordinator; + LinkTokenInterface public linkToken; + uint64 public subscriptionId; + address public 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; + createNewSubscription(); + } + + // ************************************* // + // * 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); + } + + // ************************************* // + // * 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 + * @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() public 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); + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * @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); +} 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); +} 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; +} 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"); + } +} diff --git a/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol new file mode 100644 index 000000000..616df739e --- /dev/null +++ b/contracts/src/rng/mock/VRFSubscriptionManagerV2Mock.sol @@ -0,0 +1,141 @@ +//SPDX-License-Identifier: MIT + +/** + * @authors: [@malatrax] + * @reviewers: [] + * @auditors: [] + * @bounties: [] + * @deployments: [] + */ +pragma solidity 0.8.18; + +import "./VRFCoordinatorV2InterfaceMock.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 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); + + // ************************************* // + // * Storage * // + // ************************************* // + + VRFCoordinatorV2InterfaceMock public vrfCoordinator; + uint64 public subscriptionId; + address public 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. + */ + constructor(address _governor, address _vrfCoordinator) { + vrfCoordinator = VRFCoordinatorV2InterfaceMock(_vrfCoordinator); + governor = _governor; + createNewSubscription(); + } + + // ************************************* // + // * 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); + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /** + * @dev Creates a new subscription, overriding the previous one to be manageable by the contract. + */ + function createNewSubscription() public 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; + } + + // ************************************* // + // * Public Views * // + // ************************************* // + + /** + * @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/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index 47be1689f..d4312a772 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); @@ -79,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 @@ -161,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 })); @@ -409,4 +410,69 @@ 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(); + + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address); + + // 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.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}`); + const lastBlock = await ethers.provider.getBlock(tx.blockNumber - 1); + + // Relayer tx + const tx2 = await homeGateway + .connect(await ethers.getSigner(relayer)) + .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"); + await sortitionModule.passPhase(); // Staking -> Generating + + 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/staking.ts similarity index 59% rename from contracts/test/arbitration/unstake.ts rename to contracts/test/arbitration/staking.ts index 671fe3c80..af8b2c5cc 100644 --- a/contracts/test/arbitration/unstake.ts +++ b/contracts/test/arbitration/staking.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,9 +45,11 @@ 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 () => { + 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 @@ -59,11 +65,8 @@ 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 @@ -81,4 +84,42 @@ describe("Unstake juror", async () => { expect(await core.getJurorCourtIDs(deployer)).to.be.deep.equal([]); }); + + it("Unstake inactive juror - Chainlink VRF v2", async () => { + const arbitrationCost = ONE_TENTH_ETH.mul(3); + + 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 + + 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.functions["createDispute(uint256,bytes)"](2, extraData, { value: arbitrationCost }); + + await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime + await network.provider.send("evm_mine"); + + await sortitionModule.passPhase(); // Staking -> Generating + + 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); + + 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([]); + }); }); diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index fecfe54f2..5010d101d 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -13,6 +13,8 @@ import { RandomizerRNG, RandomizerMock, SortitionModule, + VRFConsumerV2, + VRFCoordinatorV2Mock, } from "../../typechain-types"; /* eslint-disable no-unused-vars */ @@ -39,7 +41,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 +62,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; @@ -59,7 +74,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(); @@ -146,9 +161,9 @@ 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()); + await randomizer.relay(rng.address, 0, ethers.utils.randomBytes(32)); await sortitionModule.passPhase(); // Generating -> Drawing expect(await sortitionModule.phase()).to.equal(Phase.drawing); @@ -184,6 +199,134 @@ describe("Integration tests", async () => { 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(); + + await sortitionModule.changeRandomNumberGenerator(vrfConsumer.address); + + 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.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.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.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.totalStaked).to.equal(ONE_THOUSAND_PNK.mul(4)); + expect(result.totalLocked).to.equal(0); + logJurorBalance(result); + }); + 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 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( + ["bytes", "bytes32", "uint256", "address", "uint256", "uint256", "bytes"], + [ethers.utils.toUtf8Bytes("createDispute"), lastBlock.hash, 31337, arbitrable.address, disputeId, 2, "0x00"] + ); + + console.log("dispute hash: ", disputeHash); + + // Relayer tx + const tx2 = await homeGateway + .connect(relayer) + .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; + + 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 + 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()); + + 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.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); + + 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. + }); + const mineBlocks = async (n: number) => { for (let index = 0; index < n; index++) { await network.provider.send("evm_mine");