diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 8afc215c5..07201c2f5 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -9,6 +9,11 @@ The format is based on [Common Changelog](https://common-changelog.org/). ### Changed - **Breaking:** Replace `require()` with `revert()` and custom errors outside KlerosCore for consistency and smaller bytecode ([#2084](https://github.com/kleros/kleros-v2/issues/2084)) +- **Breaking:** Rename the interface from `RNG` to `IRNG` ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) +- **Breaking:** Remove the `_block` parameter from `IRNG.requestRandomness()` and `IRNG.receiveRandomness()`, not needed for the primary VRF-based RNG ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) +- Make the primary VRF-based RNG fall back to `BlockhashRNG` if the VRF request is not fulfilled within a timeout ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) +- Authenticate the calls to the RNGs to prevent 3rd parties from depleting the Chainlink VRF subscription funds ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) +- Use `block.timestamp` rather than `block.number` for `BlockhashRNG` for better reliability on Arbitrum as block production is sporadic depending on network conditions. ([#2054](https://github.com/kleros/kleros-v2/issues/2054)) - Set the Hardhat Solidity version to v0.8.30 and enable the IR pipeline ([#2069](https://github.com/kleros/kleros-v2/issues/2069)) - Set the Foundry Solidity version to v0.8.30 and enable the IR pipeline ([#2073](https://github.com/kleros/kleros-v2/issues/2073)) - Widen the allowed solc version to any v0.8.x for the interfaces only ([#2083](https://github.com/kleros/kleros-v2/issues/2083)) diff --git a/contracts/deploy/00-ethereum-pnk.ts b/contracts/deploy/00-ethereum-pnk.ts deleted file mode 100644 index 85c674fb7..000000000 --- a/contracts/deploy/00-ethereum-pnk.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { ForeignChains, HardhatChain, isSkipped } from "./utils"; - -enum Chains { - SEPOLIA = ForeignChains.ETHEREUM_SEPOLIA, - HARDHAT = HardhatChain.HARDHAT, -} - -const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId } = hre; - const { deploy } = deployments; - - // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); - console.log("deploying to %s with deployer %s", Chains[chainId], deployer); - - await deploy("PinakionV2", { - from: deployer, - args: [], - log: true, - }); -}; - -deployArbitration.tags = ["Pinakion"]; -deployArbitration.skip = async ({ network }) => { - return isSkipped(network, !Chains[network.config.chainId ?? 0]); -}; - -export default deployArbitration; diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index 45a6a7d15..68672b841 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -6,13 +6,12 @@ import { changeCurrencyRate } from "./utils/klerosCoreHelper"; import { HomeChains, isSkipped, isDevnet, PNK, ETH } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet, deployERC721 } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; const { deploy } = deployments; const { ZeroAddress } = hre.ethers; - const RNG_LOOKAHEAD = 20; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -45,7 +44,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const devnet = isDevnet(hre.network); const minStakingTime = devnet ? 180 : 1800; const maxFreezingTime = devnet ? 600 : 1800; - const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; + const rngWithFallback = await ethers.getContract("RNGWithFallback"); const maxStakePerJuror = PNK(2_000); const maxTotalStaked = PNK(2_000_000); const sortitionModule = await deployUpgradable(deployments, "SortitionModuleNeo", { @@ -55,8 +54,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) klerosCoreAddress, minStakingTime, maxFreezingTime, - rng.target, - RNG_LOOKAHEAD, + rngWithFallback.target, maxStakePerJuror, maxTotalStaked, ], @@ -94,11 +92,11 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) await disputeKitContract.changeCore(klerosCore.address); } - // rng.changeSortitionModule() only if necessary - const rngSortitionModule = await rng.sortitionModule(); - if (rngSortitionModule !== sortitionModule.address) { - console.log(`rng.changeSortitionModule(${sortitionModule.address})`); - await rng.changeSortitionModule(sortitionModule.address); + // rngWithFallback.changeConsumer() only if necessary + const rngConsumer = await rngWithFallback.consumer(); + if (rngConsumer !== sortitionModule.address) { + console.log(`rngWithFallback.changeConsumer(${sortitionModule.address})`); + await rngWithFallback.changeConsumer(sortitionModule.address); } const core = (await hre.ethers.getContract("KlerosCoreNeo")) as KlerosCoreNeo; diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts index c22a1b960..6bd763a88 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -6,13 +6,12 @@ import { changeCurrencyRate } from "./utils/klerosCoreHelper"; import { HomeChains, isSkipped, isDevnet, PNK, ETH, Courts } from "./utils"; import { getContractOrDeploy, getContractOrDeployUpgradable } from "./utils/getContractOrDeploy"; import { deployERC20AndFaucet } from "./utils/deployTokens"; -import { ChainlinkRNG, DisputeKitClassic, KlerosCore } from "../typechain-types"; +import { ChainlinkRNG, DisputeKitClassic, KlerosCore, RNGWithFallback } from "../typechain-types"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; const { deploy } = deployments; const { ZeroAddress } = hre.ethers; - const RNG_LOOKAHEAD = 20; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -50,10 +49,10 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const devnet = isDevnet(hre.network); const minStakingTime = devnet ? 180 : 1800; const maxFreezingTime = devnet ? 600 : 1800; - const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; + const rngWithFallback = await ethers.getContract("RNGWithFallback"); const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, RNG_LOOKAHEAD], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rngWithFallback.target], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -80,18 +79,18 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) }); // nonce+2 (implementation), nonce+3 (proxy) // disputeKit.changeCore() only if necessary - const disputeKitContract = (await ethers.getContract("DisputeKitClassic")) as DisputeKitClassic; + const disputeKitContract = await ethers.getContract("DisputeKitClassic"); const currentCore = await disputeKitContract.core(); if (currentCore !== klerosCore.address) { console.log(`disputeKit.changeCore(${klerosCore.address})`); await disputeKitContract.changeCore(klerosCore.address); } - // rng.changeSortitionModule() only if necessary - const rngSortitionModule = await rng.sortitionModule(); - if (rngSortitionModule !== sortitionModule.address) { - console.log(`rng.changeSortitionModule(${sortitionModule.address})`); - await rng.changeSortitionModule(sortitionModule.address); + // rngWithFallback.changeConsumer() only if necessary + const rngConsumer = await rngWithFallback.consumer(); + if (rngConsumer !== sortitionModule.address) { + console.log(`rngWithFallback.changeConsumer(${sortitionModule.address})`); + await rngWithFallback.changeConsumer(sortitionModule.address); } const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; diff --git a/contracts/deploy/00-home-chain-pnk-faucet.ts b/contracts/deploy/00-home-chain-pnk-faucet.ts deleted file mode 100644 index e10eb93bf..000000000 --- a/contracts/deploy/00-home-chain-pnk-faucet.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { HomeChains, isSkipped } from "./utils"; - -const pnkByChain = new Map([ - [HomeChains.ARBITRUM_ONE, "0x330bD769382cFc6d50175903434CCC8D206DCAE5"], - [HomeChains.ARBITRUM_SEPOLIA, "INSERT ARBITRUM SEPOLIA PNK TOKEN ADDRESS HERE"], -]); - -const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId } = hre; - const { deploy, execute } = deployments; - - // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); - console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); - - const pnkAddress = pnkByChain.get(chainId); - if (pnkAddress) { - await deploy("PNKFaucet", { - from: deployer, - contract: "Faucet", - args: [pnkAddress], - log: true, - }); - await execute("PNKFaucet", { from: deployer, log: true }, "changeAmount", hre.ethers.parseUnits("10000", "ether")); - } -}; - -deployArbitration.tags = ["PnkFaucet"]; -deployArbitration.skip = async ({ network }) => { - return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); -}; - -export default deployArbitration; diff --git a/contracts/deploy/00-home-chain-resolver.ts b/contracts/deploy/00-home-chain-resolver.ts index d7d2186ef..64d3431f6 100644 --- a/contracts/deploy/00-home-chain-resolver.ts +++ b/contracts/deploy/00-home-chain-resolver.ts @@ -1,7 +1,7 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HomeChains, isSkipped } from "./utils"; -import { deployUpgradable } from "./utils/deployUpgradable"; +import { getContractOrDeploy } from "./utils/getContractOrDeploy"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; @@ -15,7 +15,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const klerosCore = await deployments.get("KlerosCore"); const disputeTemplateRegistry = await deployments.get("DisputeTemplateRegistry"); - await deploy("DisputeResolver", { + await getContractOrDeploy(hre, "DisputeResolver", { from: deployer, args: [klerosCore.address, disputeTemplateRegistry.address], log: true, diff --git a/contracts/deploy/00-chainlink-rng.ts b/contracts/deploy/00-rng-chainlink.ts similarity index 76% rename from contracts/deploy/00-chainlink-rng.ts rename to contracts/deploy/00-rng-chainlink.ts index 1062fe936..78a1c5e87 100644 --- a/contracts/deploy/00-chainlink-rng.ts +++ b/contracts/deploy/00-rng-chainlink.ts @@ -2,10 +2,10 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HomeChains, isSkipped } from "./utils"; import { getContractOrDeploy } from "./utils/getContractOrDeploy"; +import { RNGWithFallback } from "../typechain-types"; const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId } = hre; - const { deploy } = deployments; + const { getNamedAccounts, getChainId, ethers } = hre; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -57,11 +57,16 @@ const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const requestConfirmations = 200; // between 1 and 200 L2 blocks const callbackGasLimit = 100000; - await deploy("ChainlinkRNG", { + const oldRng = await ethers.getContractOrNull("ChainlinkRNG"); + if (!oldRng) { + console.log("Register this Chainlink consumer here: http://vrf.chain.link/"); + } + + const rng = await getContractOrDeploy(hre, "ChainlinkRNG", { from: deployer, args: [ deployer, - deployer, // The consumer is configured as the SortitionModule later + deployer, // The consumer is configured as the RNGWithFallback later ChainlinkVRFCoordinator.target, keyHash, subscriptionId, @@ -70,6 +75,26 @@ const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { ], log: true, }); + + const fallbackTimeoutSeconds = 30 * 60; // 30 minutes + await getContractOrDeploy(hre, "RNGWithFallback", { + from: deployer, + args: [ + deployer, + deployer, // The consumer is configured as the SortitionModule later + fallbackTimeoutSeconds, + rng.target, + ], + log: true, + }); + + // rng.changeConsumer() only if necessary + const rngWithFallback = await ethers.getContract("RNGWithFallback"); + const rngConsumer = await rng.consumer(); + if (rngConsumer !== rngWithFallback.target) { + console.log(`rng.changeConsumer(${rngWithFallback.target})`); + await rng.changeConsumer(rngWithFallback.target); + } }; deployRng.tags = ["ChainlinkRNG"]; diff --git a/contracts/deploy/00-randomizer-rng.ts b/contracts/deploy/00-rng-randomizer.ts similarity index 54% rename from contracts/deploy/00-randomizer-rng.ts rename to contracts/deploy/00-rng-randomizer.ts index c28dc3cda..8413b39f6 100644 --- a/contracts/deploy/00-randomizer-rng.ts +++ b/contracts/deploy/00-rng-randomizer.ts @@ -2,10 +2,10 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HomeChains, isSkipped } from "./utils"; import { getContractOrDeploy } from "./utils/getContractOrDeploy"; +import { RNGWithFallback } from "../typechain-types"; const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId } = hre; - const { deploy } = deployments; + const { getNamedAccounts, getChainId, ethers } = hre; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -20,11 +20,35 @@ const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { log: true, }); - await getContractOrDeploy(hre, "RandomizerRNG", { + const rng = await getContractOrDeploy(hre, "RandomizerRNG", { from: deployer, - args: [deployer, deployer, randomizerOracle.target], // The consumer is configured as the SortitionModule later + args: [ + deployer, + deployer, // The consumer is configured as the RNGWithFallback later + randomizerOracle.target, + ], log: true, }); + + const fallbackTimeoutSeconds = 30 * 60; // 30 minutes + await getContractOrDeploy(hre, "RNGWithFallback", { + from: deployer, + args: [ + deployer, + deployer, // The consumer is configured as the SortitionModule later + fallbackTimeoutSeconds, + rng.target, + ], + log: true, + }); + + // rng.changeConsumer() only if necessary + const rngWithFallback = await ethers.getContract("RNGWithFallback"); + const rngConsumer = await rng.consumer(); + if (rngConsumer !== rngWithFallback.target) { + console.log(`rng.changeConsumer(${rngWithFallback.target})`); + await rng.changeConsumer(rngWithFallback.target); + } }; deployRng.tags = ["RandomizerRNG"]; diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts deleted file mode 100644 index 2489406c1..000000000 --- a/contracts/deploy/00-rng.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { DeployFunction } from "hardhat-deploy/types"; -import { SortitionModule } from "../typechain-types"; -import { HomeChains, isMainnet, isSkipped } from "./utils"; -import { deployUpgradable } from "./utils/deployUpgradable"; -import { getContractOrDeploy } from "./utils/getContractOrDeploy"; - -const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - const { deployments, getNamedAccounts, getChainId, ethers } = hre; - const { deploy } = deployments; - const RNG_LOOKAHEAD = 20; - - // fallback to hardhat node signers on local network - const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; - const chainId = Number(await getChainId()); - console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); - - const sortitionModule = (await ethers.getContract("SortitionModuleNeo")) as SortitionModule; - - const randomizerOracle = await getContractOrDeploy(hre, "RandomizerOracle", { - from: deployer, - contract: "RandomizerMock", - args: [], - log: true, - }); - - const rng1 = await deploy("RandomizerRNG", { - from: deployer, - args: [deployer, sortitionModule.target, randomizerOracle.address], - log: true, - }); - - const rng2 = await deploy("BlockHashRNG", { - from: deployer, - args: [], - log: true, - }); - - await sortitionModule.changeRandomNumberGenerator(rng2.address, RNG_LOOKAHEAD); -}; - -deployArbitration.tags = ["RNG"]; -deployArbitration.skip = async ({ network }) => { - return isSkipped(network, isMainnet(network)); -}; - -export default deployArbitration; diff --git a/contracts/deploy/05-arbitrable-dispute-template.ts b/contracts/deploy/change-arbitrable-dispute-template.ts similarity index 100% rename from contracts/deploy/05-arbitrable-dispute-template.ts rename to contracts/deploy/change-arbitrable-dispute-template.ts diff --git a/contracts/deploy/change-sortition-module-rng.ts b/contracts/deploy/change-sortition-module-rng.ts index a9573e6be..2b5e72435 100644 --- a/contracts/deploy/change-sortition-module-rng.ts +++ b/contracts/deploy/change-sortition-module-rng.ts @@ -23,11 +23,11 @@ const task: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { sortitionModule = await ethers.getContract("SortitionModule"); } - console.log(`chainlinkRng.changeSortitionModule(${sortitionModule.target})`); - await chainlinkRng.changeSortitionModule(sortitionModule.target); + console.log(`chainlinkRng.changeConsumer(${sortitionModule.target})`); + await chainlinkRng.changeConsumer(sortitionModule.target); - console.log(`sortitionModule.changeRandomNumberGenerator(${chainlinkRng.target}, 0)`); - await sortitionModule.changeRandomNumberGenerator(chainlinkRng.target, 0); + console.log(`sortitionModule.changeRandomNumberGenerator(${chainlinkRng.target})`); + await sortitionModule.changeRandomNumberGenerator(chainlinkRng.target); }; task.tags = ["ChangeSortitionModuleRNG"]; diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index cb4f14c58..7e881264b 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import {SortitionModuleBase, KlerosCore, RNG} from "./SortitionModuleBase.sol"; +import {SortitionModuleBase, KlerosCore, IRNG} from "./SortitionModuleBase.sol"; /// @title SortitionModule /// @dev A factory of trees that keeps track of staked values for sortition. @@ -24,16 +24,14 @@ contract SortitionModule is SortitionModuleBase { /// @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. function initialize( address _governor, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead + IRNG _rng ) external reinitializer(1) { - __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); + __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng); } function initialize4() external reinitializer(4) { diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 048fd3b40..2ad7b89b2 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -7,7 +7,7 @@ import {ISortitionModule} from "./interfaces/ISortitionModule.sol"; import {IDisputeKit} from "./interfaces/IDisputeKit.sol"; import {Initializable} from "../proxy/Initializable.sol"; import {UUPSProxiable} from "../proxy/UUPSProxiable.sol"; -import {RNG} from "../rng/RNG.sol"; +import {IRNG} from "../rng/IRNG.sol"; import "../libraries/Constants.sol"; /// @title SortitionModuleBase @@ -50,11 +50,11 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr uint256 public minStakingTime; // The time after which the phase can be switched to Drawing if there are open disputes. uint256 public maxDrawingTime; // The time after which the phase can be switched back to Staking. uint256 public lastPhaseChange; // The last time the phase was changed. - uint256 public randomNumberRequestBlock; // Number of the block when RNG request was made. + uint256 public randomNumberRequestBlock; // DEPRECATED: to be removed in the next redeploy uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. - RNG public rng; // The random number generator. + IRNG 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 rngLookahead; // DEPRECATED: to be removed in the next redeploy 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 treeHash => SortitionSumTree) sortitionSumTrees; // The mapping trees by keys. @@ -104,8 +104,7 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead + IRNG _rng ) internal onlyInitializing { governor = _governor; core = _core; @@ -113,7 +112,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr maxDrawingTime = _maxDrawingTime; lastPhaseChange = block.timestamp; rng = _rng; - rngLookahead = _rngLookahead; delayedStakeReadIndex = 1; } @@ -153,15 +151,12 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr maxDrawingTime = _maxDrawingTime; } - /// @dev Changes the `_rng` and `_rngLookahead` storage variables. - /// @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 { + /// @dev Changes the `rng` storage variable. + /// @param _rng The new random number generator. + function changeRandomNumberGenerator(IRNG _rng) external onlyByGovernor { rng = _rng; - rngLookahead = _rngLookahead; if (phase == Phase.generating) { - rng.requestRandomness(block.number + rngLookahead); - randomNumberRequestBlock = block.number; + rng.requestRandomness(); } } @@ -173,11 +168,10 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr if (phase == Phase.staking) { if (block.timestamp - lastPhaseChange < minStakingTime) revert MinStakingTimeNotPassed(); if (disputesWithoutJurors == 0) revert NoDisputesThatNeedJurors(); - rng.requestRandomness(block.number + rngLookahead); - randomNumberRequestBlock = block.number; + rng.requestRandomness(); phase = Phase.generating; } else if (phase == Phase.generating) { - randomNumber = rng.receiveRandomness(randomNumberRequestBlock + rngLookahead); + randomNumber = rng.receiveRandomness(); if (randomNumber == 0) revert RandomNumberNotReady(); phase = Phase.drawing; } else if (phase == Phase.drawing) { diff --git a/contracts/src/arbitration/SortitionModuleNeo.sol b/contracts/src/arbitration/SortitionModuleNeo.sol index b966c9379..9758882fe 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import {SortitionModuleBase, KlerosCore, RNG, StakingResult} from "./SortitionModuleBase.sol"; +import {SortitionModuleBase, KlerosCore, IRNG, StakingResult} from "./SortitionModuleBase.sol"; /// @title SortitionModuleNeo /// @dev A factory of trees that keeps track of staked values for sortition. @@ -32,7 +32,6 @@ contract SortitionModuleNeo is SortitionModuleBase { /// @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 _maxStakePerJuror The maximum amount of PNK a juror can stake in a court. /// @param _maxTotalStaked The maximum amount of PNK that can be staked in all courts. function initialize( @@ -40,12 +39,11 @@ contract SortitionModuleNeo is SortitionModuleBase { KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead, + IRNG _rng, uint256 _maxStakePerJuror, uint256 _maxTotalStaked ) external reinitializer(2) { - __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); + __SortitionModuleBase_initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng); maxStakePerJuror = _maxStakePerJuror; maxTotalStaked = _maxTotalStaked; } diff --git a/contracts/src/rng/BlockhashRNG.sol b/contracts/src/rng/BlockhashRNG.sol index 4421b4301..a36501e6e 100644 --- a/contracts/src/rng/BlockhashRNG.sol +++ b/contracts/src/rng/BlockhashRNG.sol @@ -2,43 +2,119 @@ pragma solidity ^0.8.24; -import "./RNG.sol"; +import "./IRNG.sol"; /// @title Random Number Generator using blockhash with fallback. -/// @author Clément Lesaege - /// @dev /// Random Number Generator returning the blockhash with a fallback behaviour. -/// In case no one called it within the 256 blocks, it returns the previous blockhash. -/// This contract must be used when returning 0 is a worse failure mode than returning another blockhash. -/// Allows saving the random number for use in the future. It allows the contract to still access the blockhash even after 256 blocks. -contract BlockHashRNG is RNG { - mapping(uint256 block => uint256 number) public randomNumbers; // randomNumbers[block] is the random number for this block, 0 otherwise. +/// On L2 like Arbitrum block production is sporadic so block timestamp is more reliable than block number. +/// Returns 0 when no random number is available. +/// Allows saving the random number for use in the future. It allows the contract to retrieve the blockhash even after the time window. +contract BlockHashRNG is IRNG { + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // The address that can withdraw funds. + address public consumer; // The address that can request random numbers. + uint256 public immutable lookaheadTime; // Minimal time in seconds between requesting and obtaining a random number. + uint256 public requestTimestamp; // Timestamp of the current request + mapping(uint256 timestamp => uint256 number) public randomNumbers; // randomNumbers[timestamp] is the random number for this timestamp, 0 otherwise. + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + + modifier onlyByConsumer() { + if (consumer != msg.sender) revert ConsumerOnly(); + _; + } + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @dev Constructor. + /// @param _governor The Governor of the contract. + /// @param _consumer The address that can request random numbers. + /// @param _lookaheadTime The time lookahead in seconds for the random number. + constructor(address _governor, address _consumer, uint256 _lookaheadTime) { + governor = _governor; + consumer = _consumer; + lookaheadTime = _lookaheadTime; + } + + // ************************************* // + // * Governance * // + // ************************************* // + + /// @dev Changes the governor of the contract. + /// @param _governor The new governor. + function changeGovernor(address _governor) external onlyByGovernor { + governor = _governor; + } + + /// @dev Changes the consumer of the RNG. + /// @param _consumer The new consumer. + function changeConsumer(address _consumer) external onlyByGovernor { + consumer = _consumer; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // /// @dev Request a random number. - /// @param _block Block the random number is linked to. - function requestRandomness(uint256 _block) external override { - // nop + function requestRandomness() external override onlyByConsumer { + requestTimestamp = block.timestamp; } /// @dev Return the random number. If it has not been saved and is still computable compute it. - /// @param _block Block the random number is linked to. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. - function receiveRandomness(uint256 _block) external override returns (uint256 randomNumber) { - randomNumber = randomNumbers[_block]; + function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { + if (requestTimestamp == 0) return 0; // No requests were made yet. + + uint256 expectedTimestamp = requestTimestamp + lookaheadTime; + + // Check if enough time has passed + if (block.timestamp < expectedTimestamp) { + return 0; // Not ready yet + } + + // Check if we already have a saved random number for this timestamp window + randomNumber = randomNumbers[expectedTimestamp]; if (randomNumber != 0) { return randomNumber; } - if (_block < block.number) { - // The random number is not already set and can be. - if (blockhash(_block) != 0x0) { - // Normal case. - randomNumber = uint256(blockhash(_block)); - } else { - // The contract was not called in time. Fallback to returning previous blockhash. - randomNumber = uint256(blockhash(block.number - 1)); - } + // Use last block hash for randomness + randomNumber = uint256(blockhash(block.number - 1)); + if (randomNumber != 0) { + randomNumbers[expectedTimestamp] = randomNumber; } - randomNumbers[_block] = randomNumber; + return randomNumber; + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @dev Check if randomness is ready to be received. + /// @return ready True if randomness can be received. + function isRandomnessReady() external view returns (bool ready) { + if (requestTimestamp == 0) return false; // No requests were made yet. + return block.timestamp >= requestTimestamp + lookaheadTime; + } + + /// @dev Get the timestamp when randomness will be ready. + /// @return readyTimestamp The timestamp when randomness will be available. + function getRandomnessReadyTimestamp() external view returns (uint256 readyTimestamp) { + if (requestTimestamp == 0) return 0; // No requests were made yet. + return requestTimestamp + lookaheadTime; } } diff --git a/contracts/src/rng/ChainlinkRNG.sol b/contracts/src/rng/ChainlinkRNG.sol index b829177c3..a5bff291f 100644 --- a/contracts/src/rng/ChainlinkRNG.sol +++ b/contracts/src/rng/ChainlinkRNG.sol @@ -5,17 +5,17 @@ pragma solidity ^0.8.24; import {VRFConsumerBaseV2Plus, IVRFCoordinatorV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; -import "./RNG.sol"; +import "./IRNG.sol"; /// @title Random Number Generator that uses Chainlink VRF v2.5 /// https://blog.chain.link/introducing-vrf-v2-5/ -contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { +contract ChainlinkRNG is IRNG, VRFConsumerBaseV2Plus { // ************************************* // // * Storage * // // ************************************* // address public governor; // The address that can withdraw funds. - address public sortitionModule; // The address of the SortitionModule. + address public consumer; // The address that can request random numbers. bytes32 public 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). uint256 public subscriptionId; // The unique identifier of the subscription used for funding requests. uint16 public requestConfirmations; // How many confirmations the Chainlink node should wait before responding. @@ -29,25 +29,25 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { // ************************************* // /// @dev Emitted when a request is sent to the VRF Coordinator - /// @param requestId The ID of the request - event RequestSent(uint256 indexed requestId); + /// @param _requestId The ID of the request + event RequestSent(uint256 indexed _requestId); /// Emitted when a request has been fulfilled. - /// @param requestId The ID of the request - /// @param randomWord The random value answering the request. - event RequestFulfilled(uint256 indexed requestId, uint256 randomWord); + /// @param _requestId The ID of the request + /// @param _randomWord The random value answering the request. + event RequestFulfilled(uint256 indexed _requestId, uint256 _randomWord); // ************************************* // // * Function Modifiers * // // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } - modifier onlyBySortitionModule() { - require(sortitionModule == msg.sender, "SortitionModule only"); + modifier onlyByConsumer() { + if (consumer != msg.sender) revert ConsumerOnly(); _; } @@ -57,7 +57,7 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { /// @dev Constructor, initializing the implementation to reduce attack surface. /// @param _governor The Governor of the contract. - /// @param _sortitionModule The address of the SortitionModule contract. + /// @param _consumer The address that can request random numbers. /// @param _vrfCoordinator The address of the VRFCoordinator 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. @@ -66,7 +66,7 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { /// @dev https://docs.chain.link/vrf/v2-5/subscription/get-a-random-number constructor( address _governor, - address _sortitionModule, + address _consumer, address _vrfCoordinator, bytes32 _keyHash, uint256 _subscriptionId, @@ -74,7 +74,7 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { uint32 _callbackGasLimit ) VRFConsumerBaseV2Plus(_vrfCoordinator) { governor = _governor; - sortitionModule = _sortitionModule; + consumer = _consumer; keyHash = _keyHash; subscriptionId = _subscriptionId; requestConfirmations = _requestConfirmations; @@ -91,10 +91,10 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { governor = _governor; } - /// @dev Changes the sortition module of the contract. - /// @param _sortitionModule The new sortition module. - function changeSortitionModule(address _sortitionModule) external onlyByGovernor { - sortitionModule = _sortitionModule; + /// @dev Changes the consumer of the RNG. + /// @param _consumer The new consumer. + function changeConsumer(address _consumer) external onlyByGovernor { + consumer = _consumer; } /// @dev Changes the VRF Coordinator of the contract. @@ -132,8 +132,8 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { // * State Modifiers * // // ************************************* // - /// @dev Request a random number. SortitionModule only. - function requestRandomness(uint256 /*_block*/) external override onlyBySortitionModule { + /// @dev Request a random number. Consumer only. + function requestRandomness() external override onlyByConsumer { // Will revert if subscription is not set and funded. uint256 requestId = s_vrfCoordinator.requestRandomWords( VRFV2PlusClient.RandomWordsRequest({ @@ -167,7 +167,7 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { /// @dev Return the random number. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. - function receiveRandomness(uint256 /*_block*/) external view override returns (uint256 randomNumber) { + function receiveRandomness() external view override returns (uint256 randomNumber) { randomNumber = randomNumbers[lastRequestId]; } } diff --git a/contracts/src/rng/IRNG.sol b/contracts/src/rng/IRNG.sol new file mode 100644 index 000000000..1a767fee0 --- /dev/null +++ b/contracts/src/rng/IRNG.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0 <0.9.0; + +/// @title Random Number Generator interface +interface IRNG { + /// @dev Request a random number. + function requestRandomness() external; + + /// @dev Receive the random number. + /// @return randomNumber Random number or 0 if not available + function receiveRandomness() external returns (uint256 randomNumber); + + error GovernorOnly(); + error ConsumerOnly(); +} diff --git a/contracts/src/rng/IncrementalNG.sol b/contracts/src/rng/IncrementalNG.sol index aa4b7d840..542090e71 100644 --- a/contracts/src/rng/IncrementalNG.sol +++ b/contracts/src/rng/IncrementalNG.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: MIT -/// @title Incremental Number Generator -/// @author JayBuidl -/// @dev A Random Number Generator which returns a number incremented by 1 each time. Useful as a fallback method. - pragma solidity ^0.8.24; -import "./RNG.sol"; +import "./IRNG.sol"; -contract IncrementalNG is RNG { +/// @title Incremental Number Generator +/// @dev A Random Number Generator which returns a number incremented by 1 each time. +/// For testing purposes. +contract IncrementalNG is IRNG { uint256 public number; constructor(uint256 _start) { @@ -15,15 +14,13 @@ contract IncrementalNG is RNG { } /// @dev Request a random number. - /// @param _block Block the random number is linked to. - function requestRandomness(uint256 _block) external override { + function requestRandomness() external override { // nop } /// @dev Get the "random number" (which is always the same). - /// @param _block Block the random number is linked to. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. - function receiveRandomness(uint256 _block) external override returns (uint256 randomNumber) { + function receiveRandomness() external override returns (uint256 randomNumber) { unchecked { return number++; } diff --git a/contracts/src/rng/RNG.sol b/contracts/src/rng/RNG.sol deleted file mode 100644 index 391da0075..000000000 --- a/contracts/src/rng/RNG.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.0 <0.9.0; - -interface RNG { - /// @dev Request a random number. - /// @param _block Block linked to the request. - function requestRandomness(uint256 _block) external; - - /// @dev Receive the random number. - /// @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); -} diff --git a/contracts/src/rng/RNGWithFallback.sol b/contracts/src/rng/RNGWithFallback.sol new file mode 100644 index 000000000..c47f9eb53 --- /dev/null +++ b/contracts/src/rng/RNGWithFallback.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./IRNG.sol"; + +/// @title RNG with fallback mechanism +/// @notice Uses a primary RNG implementation with automatic fallback to a Blockhash RNG if the primary RNG does not respond passed a timeout. +contract RNGWithFallback is IRNG { + // ************************************* // + // * Storage * // + // ************************************* // + + IRNG public immutable rng; // RNG address. + address public governor; // Governor address + address public consumer; // Consumer address + uint256 public fallbackTimeoutSeconds; // Time in seconds to wait before falling back to next RNG + uint256 public requestTimestamp; // Timestamp of the current request + + // ************************************* // + // * Events * // + // ************************************* // + + event RNGFallback(); + event FallbackTimeoutChanged(uint256 _newTimeout); + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @param _governor Governor address + /// @param _consumer Consumer address + /// @param _fallbackTimeoutSeconds Time in seconds to wait before falling back to next RNG + /// @param _rng The RNG address (e.g. Chainlink) + constructor(address _governor, address _consumer, uint256 _fallbackTimeoutSeconds, IRNG _rng) { + if (address(_rng) == address(0)) revert InvalidDefaultRNG(); + + governor = _governor; + consumer = _consumer; + fallbackTimeoutSeconds = _fallbackTimeoutSeconds; + rng = _rng; + } + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + if (governor != msg.sender) revert GovernorOnly(); + _; + } + + modifier onlyByConsumer() { + if (consumer != msg.sender) revert ConsumerOnly(); + _; + } + + // ************************************* // + // * Governance Functions * // + // ************************************* // + + /// @dev Change the governor + /// @param _newGovernor Address of the new governor + function changeGovernor(address _newGovernor) external onlyByGovernor { + governor = _newGovernor; + } + + /// @dev Change the consumer + /// @param _consumer Address of the new consumer + function changeConsumer(address _consumer) external onlyByGovernor { + consumer = _consumer; + } + + /// @dev Change the fallback timeout + /// @param _fallbackTimeoutSeconds New timeout in seconds + function changeFallbackTimeout(uint256 _fallbackTimeoutSeconds) external onlyByGovernor { + fallbackTimeoutSeconds = _fallbackTimeoutSeconds; + emit FallbackTimeoutChanged(_fallbackTimeoutSeconds); + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Request a random number from the primary RNG + /// @dev The consumer is trusted not to make concurrent requests. + function requestRandomness() external override onlyByConsumer { + requestTimestamp = block.timestamp; + rng.requestRandomness(); + } + + /// @dev Receive the random number from the primary RNG with fallback to the blockhash RNG if the primary RNG does not respond passed a timeout. + /// @return randomNumber Random number or 0 if not available + function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { + randomNumber = rng.receiveRandomness(); + + // If we didn't get a random number and the timeout is exceeded, try the fallback + if (randomNumber == 0 && block.timestamp > requestTimestamp + fallbackTimeoutSeconds) { + randomNumber = uint256(blockhash(block.number - 1)); + emit RNGFallback(); + } + return randomNumber; + } + + // ************************************* // + // * Errors * // + // ************************************* // + + error InvalidDefaultRNG(); +} diff --git a/contracts/src/rng/RandomizerRNG.sol b/contracts/src/rng/RandomizerRNG.sol index 940c2cf5d..96cbe6321 100644 --- a/contracts/src/rng/RandomizerRNG.sol +++ b/contracts/src/rng/RandomizerRNG.sol @@ -2,18 +2,18 @@ pragma solidity ^0.8.24; -import "./RNG.sol"; +import "./IRNG.sol"; import "./IRandomizer.sol"; /// @title Random Number Generator that uses Randomizer.ai /// https://randomizer.ai/ -contract RandomizerRNG is RNG { +contract RandomizerRNG is IRNG { // ************************************* // // * Storage * // // ************************************* // address public governor; // The address that can withdraw funds. - address public sortitionModule; // The address of the SortitionModule. + address public consumer; // The address that can request random numbers. IRandomizer public randomizer; // Randomizer address. uint256 public callbackGasLimit; // Gas limit for the Randomizer.ai callback. uint256 public lastRequestId; // The last request ID. @@ -37,12 +37,12 @@ contract RandomizerRNG is RNG { // ************************************* // modifier onlyByGovernor() { - require(governor == msg.sender, "Governor only"); + if (governor != msg.sender) revert GovernorOnly(); _; } - modifier onlyBySortitionModule() { - require(sortitionModule == msg.sender, "SortitionModule only"); + modifier onlyByConsumer() { + if (consumer != msg.sender) revert ConsumerOnly(); _; } @@ -51,11 +51,12 @@ contract RandomizerRNG is RNG { // ************************************* // /// @dev Constructor - /// @param _randomizer Randomizer contract. - /// @param _governor Governor of the contract. - constructor(address _governor, address _sortitionModule, IRandomizer _randomizer) { + /// @param _governor The Governor of the contract. + /// @param _consumer The address that can request random numbers. + /// @param _randomizer The Randomizer.ai oracle contract. + constructor(address _governor, address _consumer, IRandomizer _randomizer) { governor = _governor; - sortitionModule = _sortitionModule; + consumer = _consumer; randomizer = _randomizer; callbackGasLimit = 50000; } @@ -70,10 +71,10 @@ contract RandomizerRNG is RNG { governor = _governor; } - /// @dev Changes the sortition module of the contract. - /// @param _sortitionModule The new sortition module. - function changeSortitionModule(address _sortitionModule) external onlyByGovernor { - sortitionModule = _sortitionModule; + /// @dev Changes the consumer of the RNG. + /// @param _consumer The new consumer. + function changeConsumer(address _consumer) external onlyByGovernor { + consumer = _consumer; } /// @dev Change the Randomizer callback gas limit. @@ -98,8 +99,8 @@ contract RandomizerRNG is RNG { // * State Modifiers * // // ************************************* // - /// @dev Request a random number. SortitionModule only. - function requestRandomness(uint256 /*_block*/) external override onlyBySortitionModule { + /// @dev Request a random number. Consumer only. + function requestRandomness() external override onlyByConsumer { uint256 requestId = randomizer.request(callbackGasLimit); lastRequestId = requestId; emit RequestSent(requestId); @@ -109,7 +110,7 @@ contract RandomizerRNG is RNG { /// @param _id The ID of the request. /// @param _value The random value answering the request. function randomizerCallback(uint256 _id, bytes32 _value) external { - require(msg.sender == address(randomizer), "Randomizer only"); + if (msg.sender != address(randomizer)) revert RandomizerOnly(); randomNumbers[_id] = uint256(_value); emit RequestFulfilled(_id, uint256(_value)); } @@ -120,7 +121,13 @@ contract RandomizerRNG is RNG { /// @dev Return the random number. /// @return randomNumber The random number or 0 if it is not ready or has not been requested. - function receiveRandomness(uint256 /*_block*/) external view override returns (uint256 randomNumber) { + function receiveRandomness() external view override returns (uint256 randomNumber) { randomNumber = randomNumbers[lastRequestId]; } + + // ************************************* // + // * Errors * // + // ************************************* // + + error RandomizerOnly(); } diff --git a/contracts/src/test/RNGMock.sol b/contracts/src/test/RNGMock.sol new file mode 100644 index 000000000..df372265d --- /dev/null +++ b/contracts/src/test/RNGMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "../rng/IRNG.sol"; + +/// @title Simple mock rng to check fallback +contract RNGMock is IRNG { + uint256 public randomNumber; // The number to return; + + function setRN(uint256 _rn) external { + randomNumber = _rn; + } + + function requestRandomness() external override {} + + function receiveRandomness() external view override returns (uint256) { + return randomNumber; + } +} diff --git a/contracts/test/arbitration/dispute-kit-gated.ts b/contracts/test/arbitration/dispute-kit-gated.ts index daa78ce0c..aa8c504db 100644 --- a/contracts/test/arbitration/dispute-kit-gated.ts +++ b/contracts/test/arbitration/dispute-kit-gated.ts @@ -61,7 +61,7 @@ describe("DisputeKitGated", async () => { }); rng = (await ethers.getContract("IncrementalNG")) as IncrementalNG; - await sortitionModule.changeRandomNumberGenerator(rng.target, 20).then((tx) => tx.wait()); + await sortitionModule.changeRandomNumberGenerator(rng.target).then((tx) => tx.wait()); const hre = require("hardhat"); await deployERC721(hre, deployer, "TestERC721", "Nft721"); @@ -141,11 +141,6 @@ describe("DisputeKitGated", async () => { await network.provider.send("evm_mine"); await sortitionModule.passPhase().then((tx) => tx.wait()); // Staking -> Generating - const lookahead = await sortitionModule.rngLookahead(); - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } - await sortitionModule.passPhase().then((tx) => tx.wait()); // Generating -> Drawing return core.draw(disputeId, 70, { gasLimit: 10000000 }); }; diff --git a/contracts/test/arbitration/draw.ts b/contracts/test/arbitration/draw.ts index 12790fdb5..4d2882a57 100644 --- a/contracts/test/arbitration/draw.ts +++ b/contracts/test/arbitration/draw.ts @@ -81,7 +81,7 @@ describe("Draw Benchmark", async () => { }); rng = (await ethers.getContract("IncrementalNG")) as IncrementalNG; - await sortitionModule.changeRandomNumberGenerator(rng.target, 20).then((tx) => tx.wait()); + await sortitionModule.changeRandomNumberGenerator(rng.target).then((tx) => tx.wait()); // CourtId 2 = CHILD_COURT const minStake = 3n * 10n ** 20n; // 300 PNK @@ -174,11 +174,6 @@ describe("Draw Benchmark", async () => { await network.provider.send("evm_mine"); await sortitionModule.passPhase().then((tx) => tx.wait()); // Staking -> Generating - const lookahead = await sortitionModule.rngLookahead(); - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } - await sortitionModule.passPhase().then((tx) => tx.wait()); // Generating -> Drawing await expectFromDraw(core.draw(0, 20, { gasLimit: 1000000 })); diff --git a/contracts/test/arbitration/staking-neo.ts b/contracts/test/arbitration/staking-neo.ts index 2f1523519..696935b40 100644 --- a/contracts/test/arbitration/staking-neo.ts +++ b/contracts/test/arbitration/staking-neo.ts @@ -56,13 +56,13 @@ describe("Staking", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - pnk = (await ethers.getContract("PNK")) as PNK; - core = (await ethers.getContract("KlerosCoreNeo")) as KlerosCoreNeo; - sortition = (await ethers.getContract("SortitionModuleNeo")) as SortitionModuleNeo; - rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - vrfCoordinator = (await ethers.getContract("ChainlinkVRFCoordinator")) as ChainlinkVRFCoordinatorV2Mock; - resolver = (await ethers.getContract("DisputeResolverNeo")) as DisputeResolver; - nft = (await ethers.getContract("KlerosV2NeoEarlyUser")) as TestERC721; + pnk = await ethers.getContract("PNK"); + core = await ethers.getContract("KlerosCoreNeo"); + sortition = await ethers.getContract("SortitionModuleNeo"); + rng = await ethers.getContract("ChainlinkRNG"); + vrfCoordinator = await ethers.getContract("ChainlinkVRFCoordinator"); + resolver = await ethers.getContract("DisputeResolverNeo"); + nft = await ethers.getContract("KlerosV2NeoEarlyUser"); // Juror signer setup and funding const { firstWallet } = await getNamedAccounts(); @@ -105,10 +105,7 @@ describe("Staking", async () => { const drawFromGeneratingPhase = async () => { expect(await sortition.phase()).to.be.equal(1); // Generating - const lookahead = await sortition.rngLookahead(); - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } + await network.provider.send("evm_mine"); await vrfCoordinator.fulfillRandomWords(1, rng.target, []); await sortition.passPhase(); // Generating -> Drawing diff --git a/contracts/test/arbitration/staking.ts b/contracts/test/arbitration/staking.ts index 4d0262c22..d27a5f10e 100644 --- a/contracts/test/arbitration/staking.ts +++ b/contracts/test/arbitration/staking.ts @@ -27,11 +27,11 @@ describe("Staking", async () => { fallbackToGlobal: true, keepExistingDeployments: false, }); - pnk = (await ethers.getContract("PNK")) as PNK; - core = (await ethers.getContract("KlerosCore")) as KlerosCore; - sortition = (await ethers.getContract("SortitionModule")) as SortitionModule; - rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; - vrfCoordinator = (await ethers.getContract("ChainlinkVRFCoordinator")) as ChainlinkVRFCoordinatorV2Mock; + pnk = await ethers.getContract("PNK"); + core = await ethers.getContract("KlerosCore"); + sortition = await ethers.getContract("SortitionModule"); + rng = await ethers.getContract("ChainlinkRNG"); + vrfCoordinator = await ethers.getContract("ChainlinkVRFCoordinator"); }; describe("When outside the Staking phase", async () => { @@ -53,11 +53,8 @@ describe("Staking", async () => { await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); - const lookahead = await sortition.rngLookahead(); await sortition.passPhase(); // Staking -> Generating - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } + await network.provider.send("evm_mine"); balanceBefore = await pnk.balanceOf(deployer); }; @@ -393,11 +390,9 @@ describe("Staking", async () => { await network.provider.send("evm_increaseTime", [2000]); // Wait for minStakingTime await network.provider.send("evm_mine"); - const lookahead = await sortition.rngLookahead(); await sortition.passPhase(); // Staking -> Generating - for (let index = 0; index < lookahead; index++) { - await network.provider.send("evm_mine"); - } + await network.provider.send("evm_mine"); + await vrfCoordinator.fulfillRandomWords(1, rng.target, []); await sortition.passPhase(); // Generating -> Drawing diff --git a/contracts/test/foundry/KlerosCore.t.sol b/contracts/test/foundry/KlerosCore.t.sol index fad1db372..d0c3c190b 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -12,6 +12,8 @@ import {ISortitionModule} from "../../src/arbitration/interfaces/ISortitionModul import {SortitionModuleMock, SortitionModuleBase} from "../../src/test/SortitionModuleMock.sol"; import {UUPSProxy} from "../../src/proxy/UUPSProxy.sol"; import {BlockHashRNG} from "../../src/rng/BlockHashRNG.sol"; +import {RNGWithFallback, IRNG} from "../../src/rng/RNGWithFallback.sol"; +import {RNGMock} from "../../src/test/RNGMock.sol"; import {PNK} from "../../src/token/PNK.sol"; import {TestERC20} from "../../src/token/TestERC20.sol"; import {ArbitrableExample, IArbitrableV2} from "../../src/arbitration/arbitrables/ArbitrableExample.sol"; @@ -53,7 +55,7 @@ contract KlerosCoreTest is Test { uint256 minStakingTime; uint256 maxDrawingTime; - uint256 rngLookahead; + uint256 rngLookahead; // Time in seconds string templateData; string templateDataMappings; @@ -63,7 +65,6 @@ contract KlerosCoreTest is Test { SortitionModuleMock smLogic = new SortitionModuleMock(); DisputeKitClassic dkLogic = new DisputeKitClassic(); DisputeTemplateRegistry registryLogic = new DisputeTemplateRegistry(); - rng = new BlockHashRNG(); pinakion = new PNK(); feeToken = new TestERC20("Test", "TST"); wNative = new TestERC20("wrapped ETH", "wETH"); @@ -93,9 +94,11 @@ contract KlerosCoreTest is Test { sortitionExtraData = abi.encode(uint256(5)); minStakingTime = 18; maxDrawingTime = 24; - rngLookahead = 20; hiddenVotes = false; + rngLookahead = 30; + rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); bytes memory initDataDk = abi.encodeWithSignature( @@ -109,17 +112,18 @@ contract KlerosCoreTest is Test { disputeKit = DisputeKitClassic(address(proxyDk)); bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address,uint256)", + "initialize(address,address,uint256,uint256,address)", governor, address(proxyCore), minStakingTime, maxDrawingTime, - rng, - rngLookahead + rng ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); sortitionModule = SortitionModuleMock(address(proxySm)); + vm.prank(governor); + rng.changeConsumer(address(sortitionModule)); core = KlerosCoreMock(address(proxyCore)); core.initialize( @@ -239,11 +243,9 @@ contract KlerosCoreTest is Test { assertEq(sortitionModule.minStakingTime(), 18, "Wrong minStakingTime"); assertEq(sortitionModule.maxDrawingTime(), 24, "Wrong maxDrawingTime"); assertEq(sortitionModule.lastPhaseChange(), block.timestamp, "Wrong lastPhaseChange"); - assertEq(sortitionModule.randomNumberRequestBlock(), 0, "randomNumberRequestBlock should be 0"); assertEq(sortitionModule.disputesWithoutJurors(), 0, "disputesWithoutJurors should be 0"); assertEq(address(sortitionModule.rng()), address(rng), "Wrong RNG address"); assertEq(sortitionModule.randomNumber(), 0, "randomNumber should be 0"); - assertEq(sortitionModule.rngLookahead(), 20, "Wrong rngLookahead"); assertEq(sortitionModule.delayedStakeWriteIndex(), 0, "delayedStakeWriteIndex should be 0"); assertEq(sortitionModule.delayedStakeReadIndex(), 1, "Wrong delayedStakeReadIndex"); @@ -260,7 +262,6 @@ contract KlerosCoreTest is Test { KlerosCoreMock coreLogic = new KlerosCoreMock(); SortitionModuleMock smLogic = new SortitionModuleMock(); DisputeKitClassic dkLogic = new DisputeKitClassic(); - rng = new BlockHashRNG(); pinakion = new PNK(); governor = msg.sender; @@ -280,9 +281,11 @@ contract KlerosCoreTest is Test { sortitionExtraData = abi.encode(uint256(5)); minStakingTime = 18; maxDrawingTime = 24; - rngLookahead = 20; hiddenVotes = false; + rngLookahead = 20; + rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); bytes memory initDataDk = abi.encodeWithSignature( @@ -296,17 +299,18 @@ contract KlerosCoreTest is Test { disputeKit = DisputeKitClassic(address(proxyDk)); bytes memory initDataSm = abi.encodeWithSignature( - "initialize(address,address,uint256,uint256,address,uint256)", + "initialize(address,address,uint256,uint256,address)", governor, address(proxyCore), minStakingTime, maxDrawingTime, - rng, - rngLookahead + rng ); UUPSProxy proxySm = new UUPSProxy(address(smLogic), initDataSm); sortitionModule = SortitionModuleMock(address(proxySm)); + vm.prank(governor); + rng.changeConsumer(address(sortitionModule)); core = KlerosCoreMock(address(proxyCore)); vm.expectEmit(true, true, true, true); @@ -1015,7 +1019,7 @@ contract KlerosCoreTest is Test { assertEq(sortitionModule.disputesWithoutJurors(), 1, "Wrong disputesWithoutJurors count"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase assertEq(pinakion.balanceOf(address(core)), 1000, "Wrong token balance of the core"); @@ -1062,7 +1066,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase assertEq(pinakion.balanceOf(address(core)), 2000, "Wrong token balance of the core"); @@ -1089,7 +1093,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase uint256 disputeID = 0; @@ -1145,7 +1149,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase uint256 disputeID = 0; core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1433,7 +1437,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase vm.expectEmit(true, true, true, true); @@ -1469,7 +1473,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); // No one is staked so check that the empty addresses are not drawn. @@ -1512,7 +1516,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); // Dispute uses general court by default vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase (uint96 courtID, , , , ) = core.disputes(disputeID); @@ -1555,7 +1559,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1666,7 +1670,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1691,7 +1695,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS - 1); // Draw less to check the require later @@ -1802,7 +1806,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1829,7 +1833,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1870,7 +1874,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -1919,7 +1923,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2009,7 +2013,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2053,7 +2057,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2154,7 +2158,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); @@ -2232,7 +2236,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2266,7 +2270,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase // Split the stakers' votes. The first staker will get VoteID 0 and the second will take the rest. @@ -2278,7 +2282,7 @@ contract KlerosCoreTest is Test { core.setStake(GENERAL_COURT, 20000); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, 2); // Assign leftover votes to staker2 @@ -2407,7 +2411,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2497,7 +2501,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2543,7 +2547,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2598,7 +2602,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2688,7 +2692,7 @@ contract KlerosCoreTest is Test { core.setStake(GENERAL_COURT, 20000); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2742,7 +2746,7 @@ contract KlerosCoreTest is Test { core.setStake(GENERAL_COURT, 20000); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2781,7 +2785,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2835,7 +2839,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2878,7 +2882,7 @@ contract KlerosCoreTest is Test { arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase core.draw(disputeID, DEFAULT_NB_OF_JURORS); @@ -2977,7 +2981,7 @@ contract KlerosCoreTest is Test { vm.warp(block.timestamp + minStakingTime); sortitionModule.passPhase(); // Generating - vm.roll(block.number + rngLookahead + 1); + vm.warp(block.timestamp + rngLookahead); sortitionModule.passPhase(); // Drawing phase KlerosCoreBase.Round memory round = core.getRoundInfo(disputeID, 0); @@ -3029,4 +3033,113 @@ contract KlerosCoreTest is Test { assertEq(totalCommited, 0, "totalCommited should be 0"); assertEq(choiceCount, 3, "choiceCount should be 3"); } + + function test_RNGFallback() public { + RNGWithFallback rngFallback; + uint256 fallbackTimeout = 100; + RNGMock rngMock = new RNGMock(); + rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); + assertEq(rngFallback.governor(), msg.sender, "Wrong governor"); + assertEq(rngFallback.consumer(), address(sortitionModule), "Wrong sortition module address"); + assertEq(address(rngFallback.rng()), address(rngMock), "Wrong RNG in fallback contract"); + assertEq(rngFallback.fallbackTimeoutSeconds(), fallbackTimeout, "Wrong fallback timeout"); + assertEq(rngFallback.requestTimestamp(), 0, "Request timestamp should be 0"); + + vm.prank(governor); + sortitionModule.changeRandomNumberGenerator(rngFallback); + assertEq(address(sortitionModule.rng()), address(rngFallback), "Wrong RNG address"); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + + sortitionModule.passPhase(); // Generating + assertEq(rngFallback.requestTimestamp(), block.timestamp, "Wrong request timestamp"); + + vm.expectRevert(SortitionModuleBase.RandomNumberNotReady.selector); + sortitionModule.passPhase(); + + vm.warp(block.timestamp + fallbackTimeout + 1); + + // Pass several blocks too to see that correct block.number is still picked up. + vm.roll(block.number + 5); + + vm.expectEmit(true, true, true, true); + emit RNGWithFallback.RNGFallback(); + sortitionModule.passPhase(); // Drawing phase + + assertEq(sortitionModule.randomNumber(), uint256(blockhash(block.number - 1)), "Wrong random number"); + } + + function test_RNGFallback_happyPath() public { + RNGWithFallback rngFallback; + uint256 fallbackTimeout = 100; + RNGMock rngMock = new RNGMock(); + rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); + + vm.prank(governor); + sortitionModule.changeRandomNumberGenerator(rngFallback); + assertEq(address(sortitionModule.rng()), address(rngFallback), "Wrong RNG address"); + + vm.prank(staker1); + core.setStake(GENERAL_COURT, 20000); + vm.prank(disputer); + arbitrable.createDispute{value: feeForJuror * DEFAULT_NB_OF_JURORS}("Action"); + vm.warp(block.timestamp + minStakingTime); + + assertEq(rngFallback.requestTimestamp(), 0, "Request timestamp should be 0"); + + sortitionModule.passPhase(); // Generating + assertEq(rngFallback.requestTimestamp(), block.timestamp, "Wrong request timestamp"); + + rngMock.setRN(123); + + sortitionModule.passPhase(); // Drawing phase + assertEq(sortitionModule.randomNumber(), 123, "Wrong random number"); + } + + function test_RNGFallback_sanityChecks() public { + RNGWithFallback rngFallback; + uint256 fallbackTimeout = 100; + RNGMock rngMock = new RNGMock(); + rngFallback = new RNGWithFallback(msg.sender, address(sortitionModule), fallbackTimeout, rngMock); + + vm.expectRevert(IRNG.ConsumerOnly.selector); + vm.prank(governor); + rngFallback.requestRandomness(); + + vm.expectRevert(IRNG.ConsumerOnly.selector); + vm.prank(governor); + rngFallback.receiveRandomness(); + + vm.expectRevert(IRNG.GovernorOnly.selector); + vm.prank(other); + rngFallback.changeGovernor(other); + vm.prank(governor); + rngFallback.changeGovernor(other); + assertEq(rngFallback.governor(), other, "Wrong governor"); + + // Change governor back for convenience + vm.prank(other); + rngFallback.changeGovernor(governor); + + vm.expectRevert(IRNG.GovernorOnly.selector); + vm.prank(other); + rngFallback.changeConsumer(other); + vm.prank(governor); + rngFallback.changeConsumer(other); + assertEq(rngFallback.consumer(), other, "Wrong consumer"); + + vm.expectRevert(IRNG.GovernorOnly.selector); + vm.prank(other); + rngFallback.changeFallbackTimeout(5); + + vm.prank(governor); + vm.expectEmit(true, true, true, true); + emit RNGWithFallback.FallbackTimeoutChanged(5); + rngFallback.changeFallbackTimeout(5); + assertEq(rngFallback.fallbackTimeoutSeconds(), 5, "Wrong fallback timeout"); + } } diff --git a/contracts/test/integration/index.ts b/contracts/test/integration/index.ts index 395b9ed8e..5b4b7ea9a 100644 --- a/contracts/test/integration/index.ts +++ b/contracts/test/integration/index.ts @@ -9,8 +9,6 @@ import { HomeGateway, VeaMock, DisputeKitClassic, - RandomizerRNG, - RandomizerMock, SortitionModule, ChainlinkRNG, ChainlinkVRFCoordinatorV2Mock, @@ -161,7 +159,6 @@ describe("Integration tests", async () => { console.log("KC phase: %d", await sortitionModule.phase()); await sortitionModule.passPhase(); // Staking -> Generating - await mineBlocks(ethers.getNumber(await sortitionModule.rngLookahead())); // Wait for finality expect(await sortitionModule.phase()).to.equal(Phase.generating); console.log("KC phase: %d", await sortitionModule.phase()); await vrfCoordinator.fulfillRandomWords(1, rng.target, []); @@ -206,6 +203,6 @@ describe("Integration tests", async () => { }; }); -const logJurorBalance = async (result) => { +const logJurorBalance = async (result: { totalStaked: bigint; totalLocked: bigint }) => { console.log("staked=%s, locked=%s", ethers.formatUnits(result.totalStaked), ethers.formatUnits(result.totalLocked)); }; diff --git a/contracts/test/proxy/index.ts b/contracts/test/proxy/index.ts index 6b66a27fb..410753f97 100644 --- a/contracts/test/proxy/index.ts +++ b/contracts/test/proxy/index.ts @@ -4,7 +4,7 @@ import { DeployResult } from "hardhat-deploy/types"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { deployUpgradable } from "../../deploy/utils/deployUpgradable"; import { UpgradedByInheritanceV1, UpgradedByInheritanceV2 } from "../../typechain-types"; -import { UpgradedByRewrite as UpgradedByRewriteV1 } from "../../typechain-types/src/proxy/mock/by-rewrite"; +import { UpgradedByRewrite as UpgradedByRewriteV1 } from "../../typechain-types/src/proxy/mock/by-rewrite/UpgradedByRewrite.sol"; import { UpgradedByRewrite as UpgradedByRewriteV2 } from "../../typechain-types/src/proxy/mock/by-rewrite/UpgradedByRewriteV2.sol"; let deployer: HardhatEthersSigner; diff --git a/contracts/test/rng/index.ts b/contracts/test/rng/index.ts index 3d4906721..3bb50fd7c 100644 --- a/contracts/test/rng/index.ts +++ b/contracts/test/rng/index.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { deployments, ethers, network } from "hardhat"; +import { deployments, ethers, getNamedAccounts, network } from "hardhat"; import { IncrementalNG, BlockHashRNG, @@ -11,6 +11,7 @@ import { const initialNg = 424242; const abiCoder = ethers.AbiCoder.defaultAbiCoder(); +let deployer: string; describe("IncrementalNG", async () => { let rng: IncrementalNG; @@ -21,13 +22,13 @@ describe("IncrementalNG", async () => { }); it("Should return a number incrementing each time", async () => { - expect(await rng.receiveRandomness.staticCall(689376)).to.equal(initialNg); - await rng.receiveRandomness(29543).then((tx) => tx.wait()); - expect(await rng.receiveRandomness.staticCall(5894382)).to.equal(initialNg + 1); - await rng.receiveRandomness(0).then((tx) => tx.wait()); - expect(await rng.receiveRandomness.staticCall(3465)).to.equal(initialNg + 2); - await rng.receiveRandomness(2n ** 255n).then((tx) => tx.wait()); - expect(await rng.receiveRandomness.staticCall(0)).to.equal(initialNg + 3); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg); + await rng.receiveRandomness().then((tx) => tx.wait()); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 1); + await rng.receiveRandomness().then((tx) => tx.wait()); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 2); + await rng.receiveRandomness().then((tx) => tx.wait()); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 3); }); }); @@ -35,26 +36,48 @@ describe("BlockHashRNG", async () => { let rng: BlockHashRNG; beforeEach("Setup", async () => { - const rngFactory = await ethers.getContractFactory("BlockHashRNG"); - rng = (await rngFactory.deploy()) as BlockHashRNG; + const [deployer] = await ethers.getSigners(); + await deployments.delete("BlockHashRNG"); + await deployments.deploy("BlockHashRNG", { + from: deployer.address, + args: [deployer.address, deployer.address, 10], // governor, consumer, lookaheadTime (seconds) + }); + rng = await ethers.getContract("BlockHashRNG"); }); - it("Should return a non-zero number for a block number in the past", async () => { - const tx = await rng.receiveRandomness(5); - const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); - await tx.wait(); - const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); - expect(rn).to.not.equal(0); - await tx.wait(); + it("Should return a non-zero number after requesting and waiting", async () => { + // First request randomness + await rng.requestRandomness(); + + // Check that it's not ready yet + expect(await rng.isRandomnessReady()).to.be.false; + + // Advance time by 10 seconds (the lookahead time) + await network.provider.send("evm_increaseTime", [10]); + await network.provider.send("evm_mine"); + + // Now it should be ready + expect(await rng.isRandomnessReady()).to.be.true; + + // Get the random number + const randomNumber = await rng.receiveRandomness.staticCall(); + expect(randomNumber).to.not.equal(0); }); - it("Should return zero for a block number in the future", async () => { - const tx = await rng.receiveRandomness(9876543210); - const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); - await tx.wait(); - const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); - expect(rn).to.equal(0); - await tx.wait(); + it("Should return 0 if randomness not requested", async () => { + const randomNumber = await rng.receiveRandomness.staticCall(); + expect(randomNumber).to.equal(0); + }); + + it("Should return 0 if not enough time has passed", async () => { + await rng.requestRandomness(); + + // Don't advance time enough + await network.provider.send("evm_increaseTime", [5]); // Only 5 seconds + await network.provider.send("evm_mine"); + + const randomNumber = await rng.receiveRandomness.staticCall(); + expect(randomNumber).to.equal(0); }); }); @@ -63,49 +86,47 @@ describe("ChainlinkRNG", async () => { let vrfCoordinator: ChainlinkVRFCoordinatorV2Mock; beforeEach("Setup", async () => { + ({ deployer } = await getNamedAccounts()); + await deployments.fixture(["ChainlinkRNG"], { fallbackToGlobal: true, keepExistingDeployments: false, }); rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; vrfCoordinator = (await ethers.getContract("ChainlinkVRFCoordinator")) as ChainlinkVRFCoordinatorV2Mock; + + await rng.changeConsumer(deployer); }); it("Should return a non-zero random number", async () => { const requestId = 1; - const expectedRn = BigInt( - ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256"], [requestId, 0])) - ); + const expectedRn = BigInt(ethers.keccak256(abiCoder.encode(["uint256", "uint256"], [requestId, 0]))); - let tx = await rng.requestRandomness(0); + let tx = await rng.requestRandomness(); await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId); tx = await vrfCoordinator.fulfillRandomWords(requestId, rng.target, []); await expect(tx).to.emit(rng, "RequestFulfilled").withArgs(requestId, expectedRn); - const rn = await rng.receiveRandomness(0); + const rn = await rng.receiveRandomness(); expect(rn).to.equal(expectedRn); await tx.wait(); }); it("Should return only the last random number when multiple requests are made", async () => { // First request - let tx = await rng.requestRandomness(0); + let tx = await rng.requestRandomness(); const requestId1 = 1; await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId1); // Second request - tx = await rng.requestRandomness(0); + tx = await rng.requestRandomness(); const requestId2 = 2; await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId2); // Generate expected random numbers - const expectedRn1 = BigInt( - ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256"], [requestId1, 0])) - ); - const expectedRn2 = BigInt( - ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256"], [requestId2, 0])) - ); + const expectedRn1 = BigInt(ethers.keccak256(abiCoder.encode(["uint256", "uint256"], [requestId1, 0]))); + const expectedRn2 = BigInt(ethers.keccak256(abiCoder.encode(["uint256", "uint256"], [requestId2, 0]))); expect(expectedRn1).to.not.equal(expectedRn2, "Random numbers should be different"); // Fulfill first request @@ -117,7 +138,7 @@ describe("ChainlinkRNG", async () => { await expect(tx).to.emit(rng, "RequestFulfilled").withArgs(requestId2, expectedRn2); // Should return only the last random number - const rn = await rng.receiveRandomness(0); + const rn = await rng.receiveRandomness(); expect(rn).to.equal(expectedRn2); await tx.wait(); }); @@ -128,12 +149,16 @@ describe("RandomizerRNG", async () => { let randomizer: RandomizerMock; beforeEach("Setup", async () => { + ({ deployer } = await getNamedAccounts()); + await deployments.fixture(["RandomizerRNG"], { fallbackToGlobal: true, keepExistingDeployments: false, }); rng = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG; randomizer = (await ethers.getContract("RandomizerOracle")) as RandomizerMock; + + await rng.changeConsumer(deployer); }); it("Should return a non-zero random number", async () => { @@ -141,25 +166,25 @@ describe("RandomizerRNG", async () => { const expectedRn = BigInt(ethers.hexlify(randomBytes)); const requestId = 1; - let tx = await rng.requestRandomness(0); + let tx = await rng.requestRandomness(); await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId); tx = await randomizer.relay(rng.target, requestId, randomBytes); await expect(tx).to.emit(rng, "RequestFulfilled").withArgs(requestId, expectedRn); - const rn = await rng.receiveRandomness(0); + const rn = await rng.receiveRandomness(); expect(rn).to.equal(expectedRn); await tx.wait(); }); it("Should return only the last random number when multiple requests are made", async () => { // First request - let tx = await rng.requestRandomness(0); + let tx = await rng.requestRandomness(); const requestId1 = 1; await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId1); // Second request - tx = await rng.requestRandomness(0); + tx = await rng.requestRandomness(); const requestId2 = 2; await expect(tx).to.emit(rng, "RequestSent").withArgs(requestId2); @@ -180,7 +205,7 @@ describe("RandomizerRNG", async () => { await expect(tx).to.emit(rng, "RequestFulfilled").withArgs(requestId2, expectedRn2); // Should return only the last random number - const rn = await rng.receiveRandomness(0); + const rn = await rng.receiveRandomness(); expect(rn).to.equal(expectedRn2); await tx.wait(); }); diff --git a/cspell.json b/cspell.json index 0ab3e541c..0fa6dc52c 100644 --- a/cspell.json +++ b/cspell.json @@ -35,6 +35,7 @@ "IERC", "Initializable", "ipfs", + "IRNG", "kleros", "linguo", "Numberish",