diff --git a/contracts/deploy/00-home-chain-arbitration-neo.ts b/contracts/deploy/00-home-chain-arbitration-neo.ts index ce4105f04..3cea15d32 100644 --- a/contracts/deploy/00-home-chain-arbitration-neo.ts +++ b/contracts/deploy/00-home-chain-arbitration-neo.ts @@ -12,7 +12,6 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const { ethers, deployments, getNamedAccounts, getChainId } = hre; const { deploy, execute } = 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; @@ -62,16 +61,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.address, - RNG_LOOKAHEAD, - maxStakePerJuror, - maxTotalStaked, - ], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.address, maxStakePerJuror, maxTotalStaked], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -107,10 +97,10 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) // rng.changeSortitionModule() only if necessary const rngContract = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG; - const currentSortitionModule = await rngContract.sortitionModule(); + const currentSortitionModule = await rngContract.consumer(); if (currentSortitionModule !== sortitionModule.address) { console.log(`rng.changeSortitionModule(${sortitionModule.address})`); - await rngContract.changeSortitionModule(sortitionModule.address); + await rngContract.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 b1cc2c6b7..d0ea3c65c 100644 --- a/contracts/deploy/00-home-chain-arbitration.ts +++ b/contracts/deploy/00-home-chain-arbitration.ts @@ -11,7 +11,6 @@ import { DisputeKitClassic, KlerosCore, RandomizerRNG } from "../typechain-types const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { ethers, deployments, getNamedAccounts, getChainId } = hre; 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; @@ -69,7 +68,7 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) const maxFreezingTime = devnet ? 600 : 1800; const sortitionModule = await deployUpgradable(deployments, "SortitionModule", { from: deployer, - args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target, RNG_LOOKAHEAD], + args: [deployer, klerosCoreAddress, minStakingTime, maxFreezingTime, rng.target], log: true, }); // nonce (implementation), nonce+1 (proxy) @@ -104,10 +103,10 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) // rng.changeSortitionModule() only if necessary const rngContract = (await ethers.getContract("RandomizerRNG")) as RandomizerRNG; - const currentSortitionModule = await rngContract.sortitionModule(); + const currentSortitionModule = await rngContract.consumer(); if (currentSortitionModule !== sortitionModule.address) { console.log(`rng.changeSortitionModule(${sortitionModule.address})`); - await rngContract.changeSortitionModule(sortitionModule.address); + await rngContract.changeConsumer(sortitionModule.address); } const core = (await hre.ethers.getContract("KlerosCore")) as KlerosCore; diff --git a/contracts/deploy/upgrade-sortition-module.ts b/contracts/deploy/upgrade-sortition-module.ts index a556d232f..921054a08 100644 --- a/contracts/deploy/upgrade-sortition-module.ts +++ b/contracts/deploy/upgrade-sortition-module.ts @@ -5,7 +5,6 @@ import { HomeChains, isSkipped } from "./utils"; const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { deployments, getNamedAccounts, getChainId } = hre; - const RNG_LOOKAHEAD = 20; // fallback to hardhat node signers on local network const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; @@ -26,7 +25,6 @@ const deployUpgradeSortitionModule: DeployFunction = async (hre: HardhatRuntimeE 1800, // minStakingTime 1800, // maxFreezingTime rng.address, - RNG_LOOKAHEAD, ], }); } catch (err) { diff --git a/contracts/src/arbitration/SortitionModule.sol b/contracts/src/arbitration/SortitionModule.sol index 4db44c5c4..5f3e86ab8 100644 --- a/contracts/src/arbitration/SortitionModule.sol +++ b/contracts/src/arbitration/SortitionModule.sol @@ -32,16 +32,14 @@ contract SortitionModule is SortitionModuleBase, UUPSProxiable, Initializable { /// @param _minStakingTime Minimal time to stake /// @param _maxDrawingTime Time after which the drawing phase can be switched /// @param _rng The random number generator. - /// @param _rngLookahead Lookahead value for rng. function initialize( address _governor, KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead + IRNG _rng ) external reinitializer(1) { - super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); + super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng); } // ************************************* // diff --git a/contracts/src/arbitration/SortitionModuleBase.sol b/contracts/src/arbitration/SortitionModuleBase.sol index 24e32e6ea..bf4887640 100644 --- a/contracts/src/arbitration/SortitionModuleBase.sol +++ b/contracts/src/arbitration/SortitionModuleBase.sol @@ -13,7 +13,7 @@ pragma solidity 0.8.24; import "./KlerosCore.sol"; import "./interfaces/ISortitionModule.sol"; import "./interfaces/IDisputeKit.sol"; -import "../rng/RNG.sol"; +import "../rng/IRNG.sol"; import "../libraries/Constants.sol"; /// @title SortitionModuleBase @@ -63,11 +63,10 @@ abstract contract SortitionModuleBase is ISortitionModule { 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 rngLookahead; // Deprecated - WARNING: it breaks the storage layout of the contract, beta cannot be upgraded! 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. @@ -94,8 +93,7 @@ abstract contract SortitionModuleBase is ISortitionModule { KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead + IRNG _rng ) internal { governor = _governor; core = _core; @@ -103,7 +101,6 @@ abstract contract SortitionModuleBase is ISortitionModule { maxDrawingTime = _maxDrawingTime; lastPhaseChange = block.timestamp; rng = _rng; - rngLookahead = _rngLookahead; delayedStakeReadIndex = 1; } @@ -137,15 +134,12 @@ abstract contract SortitionModuleBase is ISortitionModule { 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(); } } @@ -160,11 +154,10 @@ abstract contract SortitionModuleBase is ISortitionModule { "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 0ac13b890..7dc622fa7 100644 --- a/contracts/src/arbitration/SortitionModuleNeo.sol +++ b/contracts/src/arbitration/SortitionModuleNeo.sol @@ -40,7 +40,6 @@ contract SortitionModuleNeo is SortitionModuleBase, UUPSProxiable, Initializable /// @param _minStakingTime Minimal time to stake /// @param _maxDrawingTime Time after which the drawing phase can be switched /// @param _rng The random number generator. - /// @param _rngLookahead Lookahead value for rng. /// @param _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( @@ -48,12 +47,11 @@ contract SortitionModuleNeo is SortitionModuleBase, UUPSProxiable, Initializable KlerosCore _core, uint256 _minStakingTime, uint256 _maxDrawingTime, - RNG _rng, - uint256 _rngLookahead, + IRNG _rng, uint256 _maxStakePerJuror, uint256 _maxTotalStaked ) external reinitializer(2) { - super._initialize(_governor, _core, _minStakingTime, _maxDrawingTime, _rng, _rngLookahead); + super._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 37afdfb34..669744f2c 100644 --- a/contracts/src/rng/BlockhashRNG.sol +++ b/contracts/src/rng/BlockhashRNG.sol @@ -2,43 +2,47 @@ 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 { +contract BlockHashRNG is IRNG { + uint256 public immutable lookahead; // Minimal block distance between requesting and obtaining a random number. + uint256 public requestBlock; // Block number of the current request mapping(uint256 block => uint256 number) public randomNumbers; // randomNumbers[block] is the random number for this block, 0 otherwise. + constructor(uint256 _lookahead) { + lookahead = _lookahead + lookahead; + } + /// @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 { + requestBlock = block.number; } /// @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 returns (uint256 randomNumber) { + uint256 expectedBlock = requestBlock; + randomNumber = randomNumbers[expectedBlock]; if (randomNumber != 0) { return randomNumber; } - if (_block < block.number) { + if (expectedBlock < block.number) { // The random number is not already set and can be. - if (blockhash(_block) != 0x0) { + if (blockhash(expectedBlock) != 0x0) { // Normal case. - randomNumber = uint256(blockhash(_block)); + randomNumber = uint256(blockhash(expectedBlock)); } else { // The contract was not called in time. Fallback to returning previous blockhash. randomNumber = uint256(blockhash(block.number - 1)); } } - randomNumbers[_block] = randomNumber; + randomNumbers[expectedBlock] = randomNumber; } } diff --git a/contracts/src/rng/ChainlinkRNG.sol b/contracts/src/rng/ChainlinkRNG.sol index 54883b283..2a0ef6c01 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..3b5f05109 --- /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 43b304ee0..437eed6ea 100644 --- a/contracts/src/rng/IncrementalNG.sol +++ b/contracts/src/rng/IncrementalNG.sol @@ -5,9 +5,9 @@ /// @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 { +contract IncrementalNG is IRNG { uint256 public number; constructor(uint256 _start) { @@ -15,15 +15,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 0037fc5cd..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..39898d95b --- /dev/null +++ b/contracts/src/rng/RNGWithFallback.sol @@ -0,0 +1,167 @@ +// 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 + IRNG[] public rngs; // List of RNG implementations + uint256 public fallbackTimeout; // Number of blocks to wait before falling back to next RNG + uint256 public requestBlock; // Block number 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 RNGFallbackAdded(address indexed _rng); + event RNGFallbackRemoved(address indexed _rng); + event FallbackTimeoutChanged(uint256 _newTimeout); + + // ************************************* // + // * Constructor * // + // ************************************* // + + /// @param _governor Governor address + /// @param _fallbackTimeout Number of blocks to wait before falling back to next RNG + /// @param _defaultRng The default RNG + constructor(address _governor, uint256 _fallbackTimeout, IRNG _defaultRng) { + require(address(_defaultRng) != address(0), "Invalid default RNG"); + require(_fallbackTimeout > 0, "Invalid fallback timeout"); + + governor = _governor; + fallbackTimeout = _fallbackTimeout; + rngs.push(_defaultRng); + } + + // ************************************* // + // * Function Modifiers * // + // ************************************* // + + modifier onlyByGovernor() { + require(msg.sender == governor, "Governor only"); + _; + } + + // ************************************* // + // * State Modifiers * // + // ************************************* // + + /// @dev Request a random number from the default RNG + function requestRandomness() external override { + require(!isRequesting, "Request already in progress"); + _requestRandomness(); + } + + function _requestRandomness() internal { + isRequesting = true; + requestBlock = block.number; + currentRngIndex = DEFAULT_RNG; + rngs[DEFAULT_RNG].requestRandomness(); + } + + /// @dev Receive the random number with fallback logic + /// @return randomNumber Random Number + function receiveRandomness() external override 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.number > requestBlock + fallbackTimeout) { + 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; + rngs[nextIndex].requestRandomness(); + } + } + return randomNumber; + } + + // ************************************* // + // * Governance Functions * // + // ************************************* // + + /// @dev Change the governor + /// @param _newGovernor Address of the new governor + function changeGovernor(address _newGovernor) external onlyByGovernor { + require(_newGovernor != address(0), "Invalid governor"); + governor = _newGovernor; + } + + /// @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(); + } + + /// @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 _newTimeout New timeout in blocks + function changeFallbackTimeout(uint256 _newTimeout) external onlyByGovernor { + require(_newTimeout > 0, "Invalid timeout"); + fallbackTimeout = _newTimeout; + emit FallbackTimeoutChanged(_newTimeout); + } + + /// @dev Drop the pending request. + /// Useful for the governor to ensure that re-requesting a random number will not be blocked by a previous request. + function dropPendingRequest() external onlyByGovernor { + isRequesting = false; + } + + // ************************************* // + // * 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 6849247d6..ef959e35b 100644 --- a/contracts/src/rng/RandomizerRNG.sol +++ b/contracts/src/rng/RandomizerRNG.sol @@ -2,20 +2,20 @@ pragma solidity 0.8.24; -import "./RNG.sol"; +import "./IRNG.sol"; import "./IRandomizer.sol"; import "../proxy/UUPSProxiable.sol"; import "../proxy/Initializable.sol"; /// @title Random Number Generator that uses Randomizer.ai /// https://randomizer.ai/ -contract RandomizerRNG is RNG, UUPSProxiable, Initializable { +contract RandomizerRNG is IRNG, UUPSProxiable, Initializable { // ************************************* // // * 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 lastRequestId; // The last request ID. @@ -43,8 +43,8 @@ contract RandomizerRNG is RNG, UUPSProxiable, Initializable { _; } - modifier onlyBySortitionModule() { - require(sortitionModule == msg.sender, "SortitionModule only"); + modifier onlyByConsumer() { + require(consumer == msg.sender, "Consumer only"); _; } @@ -60,13 +60,9 @@ contract RandomizerRNG is RNG, UUPSProxiable, Initializable { /// @dev Initializer /// @param _randomizer Randomizer contract. /// @param _governor Governor of the contract. - function initialize( - address _governor, - address _sortitionModule, - IRandomizer _randomizer - ) external reinitializer(1) { + function initialize(address _governor, address _consumer, IRandomizer _randomizer) external reinitializer(1) { governor = _governor; - sortitionModule = _sortitionModule; + consumer = _consumer; randomizer = _randomizer; callbackGasLimit = 50000; } @@ -89,10 +85,10 @@ contract RandomizerRNG is RNG, UUPSProxiable, Initializable { 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. @@ -117,8 +113,8 @@ contract RandomizerRNG is RNG, UUPSProxiable, Initializable { // * 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); @@ -139,7 +135,7 @@ contract RandomizerRNG is RNG, UUPSProxiable, Initializable { /// @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/rng/index.ts b/contracts/test/rng/index.ts index 3a34808f2..90e462896 100644 --- a/contracts/test/rng/index.ts +++ b/contracts/test/rng/index.ts @@ -14,13 +14,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); - expect(await rng.receiveRandomness.staticCall(5894382)).to.equal(initialNg + 1); - await rng.receiveRandomness(0); - expect(await rng.receiveRandomness.staticCall(3465)).to.equal(initialNg + 2); - await rng.receiveRandomness(2n ** 255n); - expect(await rng.receiveRandomness.staticCall(0)).to.equal(initialNg + 3); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg); + await rng.receiveRandomness(); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 1); + await rng.receiveRandomness(); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 2); + await rng.receiveRandomness(); + expect(await rng.receiveRandomness.staticCall()).to.equal(initialNg + 3); }); }); @@ -29,22 +29,15 @@ describe("BlockHashRNG", async () => { beforeEach("Setup", async () => { const rngFactory = await ethers.getContractFactory("BlockHashRNG"); - rng = (await rngFactory.deploy()) as BlockHashRNG; + rng = (await rngFactory.deploy(1)) as BlockHashRNG; }); - it("Should return a non-zero number for a block number in the past", async () => { - const tx = await rng.receiveRandomness(5); + it("Should return a non-zero number for a block number", async () => { + const tx = await rng.receiveRandomness(); const trace = await network.provider.send("debug_traceTransaction", [tx.hash]); const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); expect(rn).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]); - const [rn] = abiCoder.decode(["uint"], ethers.getBytes(`${trace.returnValue}`)); - expect(rn).to.equal(0); - }); }); describe("ChainlinkRNG", async () => { @@ -66,13 +59,13 @@ describe("ChainlinkRNG", async () => { ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().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); }); }); diff --git a/cspell.json b/cspell.json index c671ffe5b..c1b2c5c5c 100644 --- a/cspell.json +++ b/cspell.json @@ -34,6 +34,7 @@ "IERC", "Initializable", "ipfs", + "IRNG", "kleros", "linguo", "Numberish", @@ -41,6 +42,7 @@ "Proxiable", "Realitio", "repartitions", + "rngs", "SEPOLIA", "solhint", "typechain",