From 00a4cc523e326d330f803942c9d9087758dc7c75 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 24 Jul 2025 20:54:58 +0200 Subject: [PATCH 1/2] feat: rng fallback --- contracts/deploy/00-chainlink-rng.ts | 2 + .../deploy/00-home-chain-arbitration-neo.ts | 24 +-- contracts/deploy/00-home-chain-arbitration.ts | 13 +- contracts/deploy/00-home-chain-resolver.ts | 1 - contracts/deploy/00-rng.ts | 11 +- .../deploy/change-sortition-module-rng.ts | 8 +- contracts/src/arbitration/SortitionModule.sol | 8 +- .../src/arbitration/SortitionModuleBase.sol | 26 +-- .../src/arbitration/SortitionModuleNeo.sol | 8 +- contracts/src/rng/BlockhashRNG.sol | 122 +++++++++--- contracts/src/rng/ChainlinkRNG.sol | 40 ++-- contracts/src/rng/IRNG.sol | 13 ++ contracts/src/rng/IncrementalNG.sol | 17 +- contracts/src/rng/RNG.sol | 14 -- contracts/src/rng/RNGWithFallback.sol | 184 ++++++++++++++++++ contracts/src/rng/RandomizerRNG.sol | 33 ++-- .../test/arbitration/dispute-kit-gated.ts | 7 +- contracts/test/arbitration/draw.ts | 7 +- contracts/test/arbitration/staking-neo.ts | 19 +- contracts/test/arbitration/staking.ts | 21 +- contracts/test/foundry/KlerosCore.t.sol | 86 ++++---- contracts/test/integration/index.ts | 5 +- contracts/test/proxy/index.ts | 2 +- contracts/test/rng/index.ts | 100 ++++++---- contracts/test/utils/getActualAddress.ts | 2 +- cspell.json | 1 + 26 files changed, 505 insertions(+), 269 deletions(-) create mode 100644 contracts/src/rng/IRNG.sol delete mode 100644 contracts/src/rng/RNG.sol create mode 100644 contracts/src/rng/RNGWithFallback.sol diff --git a/contracts/deploy/00-chainlink-rng.ts b/contracts/deploy/00-chainlink-rng.ts index 1062fe936..a811f642b 100644 --- a/contracts/deploy/00-chainlink-rng.ts +++ b/contracts/deploy/00-chainlink-rng.ts @@ -70,6 +70,8 @@ const deployRng: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { ], log: true, }); + + console.log("Register this Chainlink consumer here: http://vrf.chain.link/"); }; deployRng.tags = ["ChainlinkRNG"]; diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index c4530604e..7fd4e7800 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -10,9 +10,8 @@ import { ChainlinkRNG, DisputeKitClassic, KlerosCoreNeo, RandomizerRNG } from ". const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; - const { deploy, execute } = deployments; + 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,16 +49,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const maxTotalStaked = PNK(2_000_000); const sortitionModule = await deployUpgradable(deployments, "SortitionModuleNeo", { from: deployer, - args: [ - deployer, - klerosCoreAddress, - minStakingTime, - maxFreezingTime, - rng.target, - RNG_LOOKAHEAD, - maxStakePerJuror, - maxTotalStaked, - ], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, maxStakePerJuror, maxTotalStaked], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -93,11 +83,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); + // rng.changeConsumer() only if necessary + const rngConsumer = await rng.consumer(); + if (rngConsumer !== sortitionModule.address) { + console.log(`rng.changeConsumer(${sortitionModule.address})`); + await rng.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 f66e14cdf..57633cb99 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -12,7 +12,6 @@ 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; @@ -53,7 +52,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const rng = (await ethers.getContract("ChainlinkRNG")) as ChainlinkRNG; const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, RNG_LOOKAHEAD], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -86,11 +85,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); + // rng.changeConsumer() only if necessary + const rngConsumer = await rng.consumer(); + if (rngConsumer !== sortitionModule.address) { + console.log(`rng.changeConsumer(${sortitionModule.address})`); + await rng.changeConsumer(sortitionModule.address); } const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; diff --git a/contracts/deploy/00-home-chain-resolver.ts b/contracts/deploy/00-home-chain-resolver.ts index d7d2186ef..5aa5e7b20 100644 --- a/contracts/deploy/00-home-chain-resolver.ts +++ b/contracts/deploy/00-home-chain-resolver.ts @@ -1,7 +1,6 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HomeChains, isSkipped } from "./utils"; -import { deployUpgradable } from "./utils/deployUpgradable"; const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; diff --git a/contracts/deploy/00-rng.ts b/contracts/deploy/00-rng.ts index 2489406c1..5eedf19b2 100644 --- a/contracts/deploy/00-rng.ts +++ b/contracts/deploy/00-rng.ts @@ -2,13 +2,12 @@ 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; + const RNG_LOOKAHEAD_TIME = 30 * 60; // 30 minutes in seconds // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -32,11 +31,15 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const rng2 = await deploy("BlockHashRNG", { from: deployer, - args: [], + args: [ + deployer, // governor + sortitionModule.target, // consumer + RNG_LOOKAHEAD_TIME, + ], log: true, }); - await sortitionModule.changeRandomNumberGenerator(rng2.address, RNG_LOOKAHEAD); + await sortitionModule.changeRandomNumberGenerator(rng2.address); }; deployArbitration.tags = ["RNG"]; 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 577d9fd22..c554c9c9c 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,9 @@ 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 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 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 +102,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 +110,6 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr maxDrawingTime = _maxDrawingTime; lastPhaseChange = block.timestamp; rng = _rng; - rngLookahead = _rngLookahead; delayedStakeReadIndex = 1; } @@ -153,15 +149,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(); } } @@ -176,11 +169,10 @@ abstract contract SortitionModuleBase is ISortitionModule, Initializable, UUPSPr "The minimum staking time has not passed yet." ); require(disputesWithoutJurors > 0, "There are no disputes that need jurors."); - 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(); require(randomNumber != 0, "Random number is not ready yet"); 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..8568de34e 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() { + require(governor == msg.sender, "Governor only"); + _; + } + + modifier onlyByConsumer() { + require(consumer == msg.sender, "Consumer only"); + _; + } + + // ************************************* // + // * 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 request made + + 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; + 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; + return requestTimestamp + lookaheadTime; } } diff --git a/contracts/src/rng/ChainlinkRNG.sol b/contracts/src/rng/ChainlinkRNG.sol index b829177c3..fe5a9ff19 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,13 +29,13 @@ 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 * // @@ -46,8 +46,8 @@ contract ChainlinkRNG is RNG, VRFConsumerBaseV2Plus { _; } - modifier onlyBySortitionModule() { - require(sortitionModule == msg.sender, "SortitionModule only"); + modifier onlyByConsumer() { + require(consumer == msg.sender, "Consumer only"); _; } @@ -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..a561029a2 --- /dev/null +++ b/contracts/src/rng/IRNG.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +/// @title Random Number Generator interface +interface IRNG { + /// @dev Request a random number. + function requestRandomness() external; + + /// @dev Receive the random number. + /// @return randomNumber Random Number. If the number is not ready or has not been required 0 instead. + function receiveRandomness() external returns (uint256 randomNumber); +} 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 5142aa0e5..000000000 --- a/contracts/src/rng/RNG.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.24; - -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..94c2b0e03 --- /dev/null +++ b/contracts/src/rng/RNGWithFallback.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./IRNG.sol"; + +/// @title RNG with fallback mechanism +/// @notice Uses multiple RNG implementations with automatic fallback if default RNG does not respond passed a timeout. +contract RNGWithFallback is IRNG { + uint256 public constant DEFAULT_RNG = 0; + + // ************************************* // + // * Storage * // + // ************************************* // + + address public governor; // Governor address + address public consumer; // Consumer address + IRNG[] public rngs; // List of RNG implementations + uint256 public fallbackTimeoutSeconds; // Time in seconds to wait before falling back to next RNG + uint256 public requestTimestamp; // Timestamp of the current request + uint256 public currentRngIndex; // Index of the current RNG + bool public isRequesting; // Whether a request is in progress + + // ************************************* // + // * Events * // + // ************************************* // + + event RNGDefaultChanged(address indexed _newDefaultRng); + event RNGFallback(uint256 _fromIndex, uint256 _toIndex); + event RNGFailure(); + event RNGFallbackAdded(address indexed _rng); + event RNGFallbackRemoved(address indexed _rng); + 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 _defaultRng The default RNG + constructor(address _governor, address _consumer, uint256 _fallbackTimeoutSeconds, IRNG _defaultRng) { + require(address(_defaultRng) != address(0), "Invalid default RNG"); + + governor = _governor; + consumer = _consumer; + fallbackTimeoutSeconds = _fallbackTimeoutSeconds; + rngs.push(_defaultRng); + } + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + require(msg.sender == governor, "Governor only"); + _; + } + + modifier onlyByConsumer() { + require(msg.sender == consumer, "Consumer only"); + _; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Request a random number from the default RNG + function requestRandomness() external override onlyByConsumer { + require(!isRequesting, "Request already in progress"); + _requestRandomness(DEFAULT_RNG); + } + + function _requestRandomness(uint256 _rngIndex) internal { + isRequesting = true; + requestTimestamp = block.timestamp; + currentRngIndex = _rngIndex; + rngs[_rngIndex].requestRandomness(); + } + + /// @dev Receive the random number with fallback logic + /// @return randomNumber Random Number + function receiveRandomness() external override onlyByConsumer returns (uint256 randomNumber) { + // Try to get random number from current RNG + randomNumber = rngs[currentRngIndex].receiveRandomness(); + + // If we got a valid number, clear the request + if (randomNumber != 0) { + isRequesting = false; + return randomNumber; + } + + // If the timeout is exceeded, try next RNG + if (block.timestamp > requestTimestamp + fallbackTimeoutSeconds) { + uint256 nextIndex = currentRngIndex + 1; + + // If we have another RNG to try, switch to it and request again + if (nextIndex < rngs.length) { + emit RNGFallback(currentRngIndex, nextIndex); + currentRngIndex = nextIndex; + _requestRandomness(nextIndex); + } else { + // No more RNGs to try + emit RNGFailure(); + } + } + return randomNumber; + } + + // ************************************* // + // * 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 default RNG + /// @param _newDefaultRng Address of the new default RNG + function changeDefaultRng(IRNG _newDefaultRng) external onlyByGovernor { + require(address(_newDefaultRng) != address(0), "Invalid RNG"); + rngs[DEFAULT_RNG] = _newDefaultRng; + emit RNGDefaultChanged(address(_newDefaultRng)); + + // Take over any pending request + _requestRandomness(DEFAULT_RNG); + } + + /// @dev Add a new RNG fallback + /// @param _newFallbackRng Address of the new RNG fallback + function addRngFallback(IRNG _newFallbackRng) external onlyByGovernor { + require(address(_newFallbackRng) != address(0), "Invalid RNG"); + rngs.push(_newFallbackRng); + emit RNGFallbackAdded(address(_newFallbackRng)); + } + + /// @dev Remove an RNG fallback + function removeLastRngFallback() external onlyByGovernor { + require(rngs.length > 1, "No fallback RNG"); + + // If the removed RNG is the current one, reset the fallback index + if (currentRngIndex > rngs.length - 2) { + currentRngIndex = DEFAULT_RNG; + } + + IRNG removedRng = rngs[rngs.length - 1]; + rngs.pop(); + emit RNGFallbackRemoved(address(removedRng)); + } + + /// @dev Change the fallback timeout + /// @param _fallbackTimeoutSeconds New timeout in seconds + function changeFallbackTimeout(uint256 _fallbackTimeoutSeconds) external onlyByGovernor { + fallbackTimeoutSeconds = _fallbackTimeoutSeconds; + emit FallbackTimeoutChanged(_fallbackTimeoutSeconds); + } + + /// @dev Emergency reset the RNG. + /// Useful for the governor to ensure that re-requesting a random number will not be blocked by a previous request. + function emergencyReset() external onlyByGovernor { + isRequesting = false; + requestTimestamp = 0; + currentRngIndex = DEFAULT_RNG; + } + + // ************************************* // + // * View Functions * // + // ************************************* // + + /// @dev Get the number of RNGs + /// @return Number of RNGs + function getRNGsCount() external view returns (uint256) { + return rngs.length; + } +} diff --git a/contracts/src/rng/RandomizerRNG.sol b/contracts/src/rng/RandomizerRNG.sol index 940c2cf5d..6db56fa3b 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. @@ -41,8 +41,8 @@ contract RandomizerRNG is RNG { _; } - modifier onlyBySortitionModule() { - require(sortitionModule == msg.sender, "SortitionModule only"); + modifier onlyByConsumer() { + require(consumer == msg.sender, "Consumer only"); _; } @@ -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); @@ -120,7 +121,7 @@ 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]; } } 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 85b0dc884..ffbdc55fa 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 e1f452da4..41c83c7a9 100644 --- a/contracts/test/foundry/KlerosCore.t.sol +++ b/contracts/test/foundry/KlerosCore.t.sol @@ -52,7 +52,7 @@ contract KlerosCoreTest is Test { uint256 minStakingTime; uint256 maxDrawingTime; - uint256 rngLookahead; + uint256 rngLookahead; // Time in seconds string templateData; string templateDataMappings; @@ -62,7 +62,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"); @@ -91,9 +90,11 @@ contract KlerosCoreTest is Test { sortitionExtraData = abi.encode(uint256(5)); minStakingTime = 18; maxDrawingTime = 24; - rngLookahead = 20; hiddenVotes = false; + rngLookahead = 600; + rng = new BlockHashRNG(msg.sender, address(sortitionModule), rngLookahead); + UUPSProxy proxyCore = new UUPSProxy(address(coreLogic), ""); bytes memory initDataDk = abi.encodeWithSignature("initialize(address,address)", governor, address(proxyCore)); @@ -102,17 +103,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( @@ -231,11 +233,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"); @@ -252,7 +252,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; @@ -272,9 +271,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("initialize(address,address)", governor, address(proxyCore)); @@ -283,17 +284,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); @@ -1001,7 +1003,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"); @@ -1048,7 +1050,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"); @@ -1075,7 +1077,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; @@ -1131,7 +1133,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); @@ -1419,7 +1421,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); @@ -1478,7 +1480,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); @@ -1521,7 +1523,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); @@ -1631,7 +1633,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); @@ -1656,7 +1658,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 @@ -1767,7 +1769,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); @@ -1794,7 +1796,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); @@ -1835,7 +1837,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); @@ -1884,7 +1886,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); @@ -1974,7 +1976,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); @@ -2018,7 +2020,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); @@ -2114,7 +2116,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); @@ -2192,7 +2194,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); @@ -2226,7 +2228,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. @@ -2238,7 +2240,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 @@ -2344,7 +2346,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); @@ -2419,7 +2421,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); @@ -2465,7 +2467,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); @@ -2520,7 +2522,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); @@ -2610,7 +2612,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); @@ -2664,7 +2666,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); @@ -2703,7 +2705,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); @@ -2757,7 +2759,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); @@ -2800,7 +2802,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); @@ -2894,7 +2896,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); 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..5e74b4a3a 100644 --- a/contracts/test/rng/index.ts +++ b/contracts/test/rng/index.ts @@ -21,13 +21,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 +35,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); }); }); @@ -73,39 +95,33 @@ describe("ChainlinkRNG", async () => { 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 +133,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(); }); @@ -141,25 +157,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 +196,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/contracts/test/utils/getActualAddress.ts b/contracts/test/utils/getActualAddress.ts index 8e38e16b3..4a5ce089f 100644 --- a/contracts/test/utils/getActualAddress.ts +++ b/contracts/test/utils/getActualAddress.ts @@ -8,7 +8,7 @@ export async function getActualAddress(network: string, contractName: string): Promise { try { const deployment = await import(`../../deployments/${network}/${contractName}.json`, { - assert: { type: "json" }, + with: { type: "json" }, }); if (!deployment.default.address) { throw new Error(`No address found in deployment file for ${contractName} on ${network}`); 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", From 5db2a51757689f701944d9b4fc8c30040fc553e8 Mon Sep 17 00:00:00 2001 From: jaybuidl Date: Thu, 24 Jul 2025 21:02:25 +0200 Subject: [PATCH 2/2] fix: re-added removed variables to preserve storage layout, marked as deprecated for later removal --- contracts/src/arbitration/SortitionModuleBase.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index c554c9c9c..7692740cc 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -50,9 +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; // DEPRECATED: to be removed in the next redeploy uint256 public disputesWithoutJurors; // The number of disputes that have not finished drawing jurors. IRNG public rng; // The random number generator. uint256 public randomNumber; // Random number returned by RNG. + 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.