diff --git a/contracts/random/README.md b/contracts/random/README.md index a4f2e48b..974c9363 100644 --- a/contracts/random/README.md +++ b/contracts/random/README.md @@ -1,64 +1,11 @@ # Random Number Generation -This directory contains contracts that provide random number generation capability using on-chain and off-chain sources. +Immutable has thoroughly investigated on-chain random number generation and use of off-chain random +providers such as Supra and Chainlink. To create a secure on-chain source requires an off-chain +service to inteact with the chain regularly. At present, there is not sufficient demand for +on-chain random for Immutable to invest in creating such a service. The source code, tests, and +threat model are contained in the [random2 branch](https://github.com/immutable/contracts/tree/random2). +In particular, teams considering using on-chain random number generation should read the +[threat model document](https://github.com/immutable/contracts/blob/random2/audits/random/202403-threat-model-random.md) as this describes in detail how on-chain randon number generation can +be used securely. -The reasons for using these contracts are that: - -* Enables you to leverage a random number generation system designed by Immutable's cryptographers. -* Allows you to build your game against an API that won't change. -* The quality of the random numbers generated will improve as new capabilities are added to the platform. That is, the migration from ```block.hash``` to ```block.prevrandao``` when the BFT fork occurs will be seamless. -* For off-chain randomness, allows you to leverage the random number provider that Immutable has agreements with. - -# Status - -Contract threat models and audits: - -| Description | Date |Version Audited | Link to Report | -|---------------------------|------------------|-----------------|----------------| -| Not audited and no threat model | - | - | - | - -## RandomSeedProvider - -The RandomSeedProvider contract has not yet been deployed. - -| Location | Date | Version Deployed | Address | -|---------------------------|------------------|------------------|---------| -| Immutable zkEVM Testnet | Not deployed | - | - | -| Immutable zkEVM Mainnet | Not deployed | - | - | - -## Architecture - -The Random Number Generation system on the immutable platform is shown in the diagram below. - -![Random number genration](./random-architecture.png) - -Game contracts extend ```RandomValues.sol```. This contract interacts with the ```RandomSeedProvider.sol``` contract to request and retreive random seed values. - -There is one ```RandomSeedProvider.sol``` contract deployed per chain. Each game has its own instance of ```RandomValues.sol``` as this contract is integrated directly into the game contract. - -The ```RandomSeedProvider.sol``` operates behind a transparent proxy, ```ERC1967Proxy.sol```, with the upgrade -logic included in the ```UUPSUpgradeable.sol``` contract that ```RandomSeedProvider.sol``` extends. Using an upgradeable pattern allows the random manager contract to be upgraded to extend its feature set and resolve issues. - -The ```RandomSeedProvider.sol``` contract can be configured to use an off-chain random number source. This source is accessed via the ```IOffchainRandomSource.sol``` interface. To allow the flexibility to switch between off-chain random sources, there is an adaptor contract between the offchain random source contract and the random seed provider. - -The architecture diagram shows a ChainLink VRF source and a Supra VRF source. This is purely to show the possibility of integrating with one off-chain service and then, at a later point choosing to switch to an alternative off-chain source. At present, there is no agreement to use any specific off-chain source. - - - -## Process of Requesting a Random Number - -The process for requesting a random number is shown below. Players do actions requiring a random number or a set of random numbers. They purchase, or commit to the random value(s), which is later revealed. - -![Random number genration](./random-sequence.png) - -The steps are: - -* The game contract calls ```_requestRandomValueCreation```. -The ```_requestRandomValueCreation``` returns a value ```_randomRequestId```. This value is supplied later to fetch the random value once it has been generated. The function ```_requestRandomValueCreation``` executes a call to the ```RandomSeedProvider``` contract requesting a seed value be produced. -* The game contract calls ```_isRandomValueReady```, passing in the ```_randomRequestId```. This returns ```READY``` if the value is ready to be returned. -* The game contract calls ```_fetchRandomValues```, passing in the ```_randomRequestId```. The random seed is returned to the ```RandomValues``` contract, which then customises the value prior returning it to the game. - - -# Notes - -Sequence diagram source [here](https://sequencediagram.org/index.html#initialData=C4S2BsFMAICUEMB2ATA9gW2gOQK7oEaQBO0A4pIsfKKogFB0AO8RoAxiM4sAOZGo5G0AMTgQPABa8ikCtABU80vHSRFTFu05Jg0AETLV0ADKoeINgDoAzqnB7o8a2RWQNrC9u76EKDADV4cBxIaxs7Byc4fzoKZHctLmkBIVFxKXxgmEUASXR0HGB4TJgALwBrAFF-AFkAHUQcxAAzIidgIhw2YBwZdWYPDiSfJDR0AGVZZAAFfgA3EGRicPtHZ1ga2JQGPhToafB4AE9iaEZetgknUMdoNr9MeG6QWjpDSABaAD5YfwAuaAAfRkAEcQtZgL4xoEsgBhGTUF6IAAUgOsIFKkAAlHRft8NgDQeDIaMMJNIMhkTjfgAeD4fAlA5o4cDNEDgVTcHLIAA0QNsFzc7zpDP+QPuY1gkDBoWA3K28ToiFQwBgqDmp3ePIJAHV4GBoM1UCR4NBMqg2OVoMBUGaYIx+MguhToMbHIhXc1mh9LvqPRKMNbbYRoEsxBqZMhLDt+IJoLCJJBLXdSZg5kEQtAQM4Ecgjm9XPixYDs1CAhnIFL4HnUQH0FKZRDudT-PiagDS6nycgqzWGoDmaz2Zy5bz+QIiGxsbj-CLGR0QgXVHOxbBKgBBAAiAE0FTG9u9Q5Bw8RnAB6O6QDVBHOpxzPV7vIsAgeQYCXMvoGHg2uphvE5sZzbAEeDfT9u2RftBzZDkKFHPk0QnKcWzndtxS7KYgJ+MVplPWggmzGA62gRA8EIEgGnwI5oB4Vw+UYQ4TiIPlRkvRtdFIghliXSAVxfOtv1CPcGBEoA). \ No newline at end of file diff --git a/contracts/random/RandomSeedProvider.sol b/contracts/random/RandomSeedProvider.sol deleted file mode 100644 index 581b19f7..00000000 --- a/contracts/random/RandomSeedProvider.sol +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2 -pragma solidity 0.8.19; - -import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable-4.9.3/proxy/utils/UUPSUpgradeable.sol"; -import {AccessControlEnumerableUpgradeable} from "openzeppelin-contracts-upgradeable-4.9.3/access/AccessControlEnumerableUpgradeable.sol"; -import {IOffchainRandomSource} from "./offchainsources/IOffchainRandomSource.sol"; - -/** - * @notice Contract to provide random seed values to game contracts on the chain. - * @dev The expectation is that there will only be one RandomSeedProvider per chain. - * Game contracts will call this contract to obtain a seed value, from which - * they will generate random values. - * - * The contract is upgradeable. It is expected to be operated behind an - * Open Zeppelin ERC1967Proxy. - */ -contract RandomSeedProvider is AccessControlEnumerableUpgradeable, UUPSUpgradeable { - /// @notice Indicate that the requested upgrade is not possible. - error CanNotUpgradeFrom(uint256 _storageVersion, uint256 _codeVersion); - - /// @notice The random seed value is not yet available. - error WaitForRandom(); - - /// @notice The off-chain random source has been updated. - event OffchainRandomSourceSet(address _offchainRandomSource); - - /// @notice Indicates that new random values from the on-chain source will be generated - /// @notice using the RanDAO source. - event RanDaoEnabled(); - - /// @notice Indicates that a game contract that can consume off-chain random has been added. - event OffchainRandomConsumerAdded(address _consumer); - - /// @notice Indicates that a game contract that can consume off-chain random has been removed. - event OffchainRandomConsumerRemoved(address _consumer); - - // Code and storage layout version number. - uint256 internal constant VERSION0 = 0; - - /// @notice Admin role that can enable RanDAO and off-chain random sources. - bytes32 public constant RANDOM_ADMIN_ROLE = keccak256("RANDOM_ADMIN_ROLE"); - - /// @notice Only accounts with UPGRADE_ADMIN_ROLE can upgrade the contract. - bytes32 public constant UPGRADE_ADMIN_ROLE = bytes32("UPGRADE_ROLE"); - - /// @notice Indicates: Generate new random numbers using on-chain methodology. - address public constant ONCHAIN = address(1); - - /// @notice All historical random output. - /// @dev When random seeds are requested, a request id is returned. The id - /// @dev relates to a certain future random seed. This map holds all of the - /// @dev random seeds that have been produced. - mapping(uint256 requestId => bytes32 randomValue) public randomOutput; - - /// @notice The index of the next seed value to be produced. - uint256 public nextRandomIndex; - - /// @notice The block number in which the last seed value was generated. - uint256 public lastBlockRandomGenerated; - - /// @notice The block when the last off-chain random request occurred. - /// @dev This is used to limit off-chain random requests to once per block. - uint256 private lastBlockOffchainRequest; - - /// @notice The request id returned in the previous off-chain random request. - /// @dev This is used to limit off-chain random requests to once per block. - uint256 private prevOffchainRandomRequest; - - /// @notice The source of new random numbers. This could be the special value ONCHAIN - /// @notice or the address of a Offchain Random Source contract. - /// @dev This value is return with the request ids. This allows off-chain random sources - /// @dev to be switched without stopping in-flight random values from being retrieved. - address public randomSource; - - /// @notice Indicates that this blockchain supports the PREVRANDAO opcode and that - /// @notice PREVRANDAO should be used rather than block hash for on-chain random values. - bool public ranDaoAvailable; - - /// @notice Indicates an address is allow listed for the off-chain random provider. - /// @dev Having an allow list prevents spammers from requesting one random number per block, - /// @dev thus incurring cost on Immutable for no benefit. - mapping(address gameContract => bool approved) public approvedForOffchainRandom; - - // @notice The version of the storage layout. - // @dev This storage slot will be used during upgrades. - uint256 public version; - - /** - * @notice Initialize the contract for use with a transparent proxy. - * @param _roleAdmin is the account that can add and remove addresses that have - * RANDOM_ADMIN_ROLE privilege. - * @param _randomAdmin is the account that has RANDOM_ADMIN_ROLE privilege. - * @param _upgradeAdmin is the account that has UPGRADE_ADMIN_ROLE privilege. - * @param _ranDaoAvailable indicates if the chain supports the PREVRANDAO opcode. - */ - function initialize( - address _roleAdmin, - address _randomAdmin, - address _upgradeAdmin, - bool _ranDaoAvailable - ) public virtual initializer { - _grantRole(DEFAULT_ADMIN_ROLE, _roleAdmin); - _grantRole(RANDOM_ADMIN_ROLE, _randomAdmin); - _grantRole(UPGRADE_ADMIN_ROLE, _upgradeAdmin); - - // Generate an initial "random" seed. - // Use the chain id as an input into the random number generator to ensure - // all random numbers are personalised to this chain. - randomOutput[0] = keccak256(abi.encodePacked(block.chainid, blockhash(block.number - 1))); - nextRandomIndex = 1; - lastBlockRandomGenerated = block.number; - - randomSource = ONCHAIN; - ranDaoAvailable = _ranDaoAvailable; - - version = VERSION0; - } - - /** - * @notice Called during contract upgrade. - * @dev This function will be overridden in future versions of this contract. - */ - function upgrade() external virtual { - // Revert in the following situations: - // - The function is called on the existing version. - // - This version of code is mistakenly deploy, for an upgrade from V2 to V3. - // That is, we mistakenly attempt to downgrade the contract. - revert CanNotUpgradeFrom(version, VERSION0); - } - - /** - * @notice Change the off-chain random source. - * @dev Only RANDOM_ADMIN_ROLE can do this. - * @param _offchainRandomSource Address of contract that is an off-chain random source. - */ - function setOffchainRandomSource(address _offchainRandomSource) external onlyRole(RANDOM_ADMIN_ROLE) { - // slither-disable-next-line missing-zero-check - randomSource = _offchainRandomSource; - emit OffchainRandomSourceSet(_offchainRandomSource); - } - - /** - * @notice Call this when the blockchain supports the PREVRANDAO opcode. - * @dev Only RANDOM_ADMIN_ROLE can do this. - */ - function setRanDaoAvailable() external onlyRole(RANDOM_ADMIN_ROLE) { - ranDaoAvailable = true; - emit RanDaoEnabled(); - } - - /** - * @notice Add a consumer that can use off-chain supplied random. - * @dev Only RANDOM_ADMIN_ROLE can do this. - * @param _consumer Game contract that inherits from RandomValues.sol that is authorised to use off-chain random. - */ - function addOffchainRandomConsumer(address _consumer) external onlyRole(RANDOM_ADMIN_ROLE) { - approvedForOffchainRandom[_consumer] = true; - emit OffchainRandomConsumerAdded(_consumer); - } - - /** - * @notice Remove a consumer that can use off-chain supplied random. - * @dev Only RANDOM_ADMIN_ROLE can do this. - * @param _consumer Game contract that inherits from RandomValues.sol that is no longer authorised to use off-chain random. - */ - function removeOffchainRandomConsumer(address _consumer) external onlyRole(RANDOM_ADMIN_ROLE) { - approvedForOffchainRandom[_consumer] = false; - emit OffchainRandomConsumerRemoved(_consumer); - } - - /** - * @notice Request the index number to track when a random number will be produced. - * @dev Note that the same _randomFulfilmentIndex will be returned to multiple games and even within - * @dev the one game. Games must personalise this value to their own game, the particular game player, - * @dev and to the game player's request. - * @return _randomFulfilmentIndex The index for the game contract to present to fetch the next random value. - * @return _randomSource Indicates that an on-chain source was used, or is the address of an off-chain source. - */ - // slither-disable-next-line reentrancy-benign, reentrancy-no-eth - function requestRandomSeed() external returns (uint256 _randomFulfilmentIndex, address _randomSource) { - if (randomSource == ONCHAIN || !approvedForOffchainRandom[msg.sender]) { - // Generate a value for this block if one has not been generated yet. This - // is required because there may have been calls to requestRandomSeed - // in previous blocks that are waiting for a random number to be produced. - _generateNextRandomOnChain(); - - // Indicate that a value based on the next block will be fine. - _randomFulfilmentIndex = nextRandomIndex; - - _randomSource = ONCHAIN; - } else { - // Limit how often off-chain random numbers are requested to a maximum of once per block. - // slither-disable-next-line incorrect-equality - if (lastBlockOffchainRequest == block.number) { - _randomFulfilmentIndex = prevOffchainRandomRequest; - } else { - lastBlockOffchainRequest = block.number; - _randomFulfilmentIndex = IOffchainRandomSource(randomSource).requestOffchainRandom(); - prevOffchainRandomRequest = _randomFulfilmentIndex; - } - _randomSource = randomSource; - } - } - - /** - * @notice Fetches a random seed value that was requested using the requestRandomSeed function. - * @dev Note that the same _randomSeed will be returned to multiple games and even within - * @dev the one game. Games must personalise this value to their own game, the particular game player, - * @dev and to the game player's request. - * @param _randomFulfilmentIndex Index indicating which random seed to return. - * @param _randomSource The source to use when retrieving the random seed. - * @return _randomSeed The value from which random values can be derived. - */ - function getRandomSeed( - uint256 _randomFulfilmentIndex, - address _randomSource - ) external returns (bytes32 _randomSeed) { - if (_randomSource == ONCHAIN) { - _generateNextRandomOnChain(); - if (_randomFulfilmentIndex >= nextRandomIndex) { - revert WaitForRandom(); - } - return randomOutput[_randomFulfilmentIndex]; - } else { - // If random source is not the address of a valid contract this will revert - // with no revert information returned. - return IOffchainRandomSource(_randomSource).getOffchainRandom(_randomFulfilmentIndex); - } - } - - /** - * @notice Check whether a random seed is ready. - * @param _randomFulfilmentIndex Index indicating which random seed to check the status of. - * @param _randomSource The source to use when retrieving the status of the random seed. - * @return bool indicates a random see is ready to be fetched. - */ - function isRandomSeedReady(uint256 _randomFulfilmentIndex, address _randomSource) external view returns (bool) { - if (_randomSource == ONCHAIN) { - // slither-disable-next-line incorrect-equality - if (lastBlockRandomGenerated == block.number) { - return _randomFulfilmentIndex < nextRandomIndex; - } else { - return _randomFulfilmentIndex < nextRandomIndex + 1; - } - } else { - return IOffchainRandomSource(_randomSource).isOffchainRandomReady(_randomFulfilmentIndex); - } - } - - /** - * @notice Check that msg.sender is authorised to perform the contract upgrade. - */ - // solhint-disable no-empty-blocks - function _authorizeUpgrade( - address newImplementation - ) internal override(UUPSUpgradeable) onlyRole(UPGRADE_ADMIN_ROLE) { - // Nothing to do beyond upgrade authorisation check. - } - // solhint-enable no-empty-blocks - - /** - * @notice Generate a random value using on-chain methodologies. - */ - function _generateNextRandomOnChain() private { - // Onchain random values can only be generated once per block. - // slither-disable-next-line incorrect-equality - if (lastBlockRandomGenerated == block.number) { - return; - } - - uint256 entropy; - if (ranDaoAvailable) { - // PrevRanDAO (previously known as DIFFICULTY) is the output of the RanDAO function - // used as a part of consensus. The value posted is the value revealed in the previous - // block, not in this block. In this way, all parties know the value prior to it being - // useable by applications. - // - // The RanDAO value can be influenced by a block producer deciding to produce or - // not produce a block. This limits the block producer's influence to one of two - // values. - // - // Prior to the BFT fork (expected in 2024), this value will be a predictable value - // related to the block number. - entropy = block.prevrandao; - } else { - // Block hash will be different for each block and difficult for game players - // to guess. However, game players can observe blocks as they are produced. - // The block producer could manipulate the block hash by crafting a - // transaction that included a number that the block producer controls. A - // malicious block producer could produce many candidate blocks, in an attempt - // to produce a specific value. - entropy = uint256(blockhash(block.number - 1)); - } - - bytes32 prevRandomOutput = randomOutput[nextRandomIndex - 1]; - bytes32 newRandomOutput = keccak256(abi.encodePacked(prevRandomOutput, entropy)); - randomOutput[nextRandomIndex++] = newRandomOutput; - lastBlockRandomGenerated = block.number; - } - - // slither-disable-next-line unused-state,naming-convention - uint256[100] private __gapRandomSeedProvider; -} diff --git a/contracts/random/RandomValues.sol b/contracts/random/RandomValues.sol deleted file mode 100644 index 9b29e61f..00000000 --- a/contracts/random/RandomValues.sol +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2 -pragma solidity 0.8.19; - -import {RandomSeedProvider} from "./RandomSeedProvider.sol"; - -/** - * @notice Game contracts that need random numbers should extend this contract. - * @dev This contract can be used with UPGRADEABLE or NON-UNGRADEABLE contracts. - */ -// slither-disable-start dead-code -abstract contract RandomValues { - /// @notice Caused by requesting random values be generated, but then setting the size to zero. - error RequestForNoRandomBytes(); - - /// @notice Caused by fetch being called more than once for the same request id. - error RandomValuesPreviouslyFetched(); - - /// @notice Structure for a single random request. - struct RandomRequest { - // Id to match the random seed provider requests and responses. - uint256 fulfilmentId; - // Number of words requested. Retaining the size ensures the correct - // number of words are returned. - uint16 size; - // Source of the random value: which off-chain, or the on-chain provider - // will provide the random values. Retaining the source allows for upgrade - // of sources inside the random seed provider contract. - address source; - } - - /// @notice Status of a random request - enum RequestStatus { - // The random value is being produced. - IN_PROGRESS, - // The random value is ready to be fetched. - READY, - // The random value either was never requested or has previously been fetched. - ALREADY_FETCHED - } - - // Address of random seed provider contract. - // This value "immutable", and hence patched directly into bytecode when it is used. - // There will only ever be one random seed provider per chain. Hence, this value - // does not need to be changed. - RandomSeedProvider public immutable randomSeedProvider; - - // Map of request id to random creation requests. - mapping(uint256 requestId => RandomRequest request) private randCreationRequests; - - // Each request has a unique request id. The id is the current value - // of nextNonce. nextNonce is incremented for each request. - uint256 private nextNonce; - - /** - * @notice Set the address of the random seed provider. - * @param _randomSeedProvider Address of random seed provider. - */ - constructor(address _randomSeedProvider) { - randomSeedProvider = RandomSeedProvider(_randomSeedProvider); - } - - /** - * @notice Register a request to generate a random value. This function should be called - * when a game player has purchased an item the value of which is based on a random value. - * @param _size The number of values to generate. - * @return _randomRequestId A value that needs to be presented when fetching the random - * value with fetchRandom. - */ - // slither-disable-next-line reentrancy-benign - function _requestRandomValueCreation(uint16 _size) internal returns (uint256 _randomRequestId) { - if (_size == 0) { - revert RequestForNoRandomBytes(); - } - - uint256 randomFulfilmentIndex; - address randomSource; - (randomFulfilmentIndex, randomSource) = randomSeedProvider.requestRandomSeed(); - _randomRequestId = nextNonce++; - randCreationRequests[_randomRequestId] = RandomRequest(randomFulfilmentIndex, _size, randomSource); - } - - /** - * @notice Fetch a set of random values that were requested using _requestRandomValueCreation. - * @dev The values are customised to this game, the game player, and the request by the game player. - * This level of personalisation ensures that no two players end up with the same random value - * and no game player will have the same random value twice. - * @dev Note that the numbers can only be requested once. The numbers are deleted once fetched. - * This has been done to ensure games don't inadvertantly reuse the same random values - * in different contexts. Reusing random values could make games vulnerable to attacks - * where game players know the expected random values. - * @param _randomRequestId The value returned by _requestRandomValueCreation. - * @return _randomValues An array of random values. - */ - // slither-disable-next-line reentrancy-benign - function _fetchRandomValues(uint256 _randomRequestId) internal returns (bytes32[] memory _randomValues) { - RandomRequest memory request = randCreationRequests[_randomRequestId]; - if (request.size == 0) { - revert RandomValuesPreviouslyFetched(); - } - // Prevent random values from being re-fetched. This reduces the probability - // that a game will mistakenly re-use the same random values for two purposes. - delete randCreationRequests[_randomRequestId]; - - // Request the random seed. If not enough time has elapsed yet, this call will revert. - bytes32 randomSeed = randomSeedProvider.getRandomSeed(request.fulfilmentId, request.source); - - // Generate the personlised seed by combining: - // address(this): personalises the random seed to this game. - // msg.sender: personalises the random seed to the game player. - // _randomRequestId: Ensures that even if the game player has requested multiple random values, - // they will get a different value for each request. - // randomSeed: Value returned by the RandomManager. - bytes32 seed = keccak256(abi.encodePacked(address(this), msg.sender, _randomRequestId, randomSeed)); - - _randomValues = new bytes32[](request.size); - for (uint256 i = 0; i < request.size; i++) { - _randomValues[i] = keccak256(abi.encodePacked(seed, i)); - } - } - - /** - * @notice Check whether a set of random values are ready to be fetched - * @dev If this function returns true then it is safe to call _fetchRandom or _fetchRandomValues. - * @param _randomRequestId The value returned by _requestRandomValueCreation. - * @return RequestStatus indicates whether the random values are still be generated, are ready - * to be fetched, or whether they have already been fetched and are no longer available. - */ - function _isRandomValueReady(uint256 _randomRequestId) internal view returns (RequestStatus) { - RandomRequest memory request = randCreationRequests[_randomRequestId]; - if (request.size == 0) { - return RequestStatus.ALREADY_FETCHED; - } - return - randomSeedProvider.isRandomSeedReady(request.fulfilmentId, request.source) - ? RequestStatus.READY - : RequestStatus.IN_PROGRESS; - } - - // slither-disable-next-line unused-state,naming-convention - uint256[100] private __gapRandomValues; -} -// slither-disable-end dead-code diff --git a/contracts/random/offchainsources/IOffchainRandomSource.sol b/contracts/random/offchainsources/IOffchainRandomSource.sol deleted file mode 100644 index f6750bbb..00000000 --- a/contracts/random/offchainsources/IOffchainRandomSource.sol +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2 -pragma solidity 0.8.19; - -/** - * @notice Off-chain random source adaptors must implement this interface. - */ -interface IOffchainRandomSource { - // The random seed value is not yet available. - error WaitForRandom(); - - /** - * @notice Request that an offchain random value be generated. - * @return _fulfilmentIndex Value to be used to fetch the random value in getOffchainRandom. - */ - function requestOffchainRandom() external returns (uint256 _fulfilmentIndex); - - /** - * @notice Fetch the latest off-chain generated random value. - * @param _fulfilmentIndex Number previously given when requesting a ramdon value. - * @return _randomValue The value generated off-chain. - */ - function getOffchainRandom(uint256 _fulfilmentIndex) external view returns (bytes32 _randomValue); - - /** - * @notice Check to see if the random value is available yet. - * @param _fulfilmentIndex Number previously given when requesting a ramdon value. - * @return true if the value is available. - */ - function isOffchainRandomReady(uint256 _fulfilmentIndex) external view returns (bool); -} diff --git a/contracts/random/offchainsources/SourceAdaptorBase.sol b/contracts/random/offchainsources/SourceAdaptorBase.sol deleted file mode 100644 index 693c14d9..00000000 --- a/contracts/random/offchainsources/SourceAdaptorBase.sol +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2 -pragma solidity 0.8.19; - -import {AccessControlEnumerable} from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; -import {IOffchainRandomSource} from "./IOffchainRandomSource.sol"; - -/** - * @notice All Verifiable Random Function (VRF) source adaptors derive from this contract. - * @dev This contract is NOT upgradeable. If there is an issue with this code, deploy a new - * version of the code and have the random seed provider point to the new version. - */ -abstract contract SourceAdaptorBase is AccessControlEnumerable, IOffchainRandomSource { - error UnexpectedRandomWordsLength(uint256 _length); - - bytes32 internal constant CONFIG_ADMIN_ROLE = keccak256("CONFIG_ADMIN_ROLE"); - - // Immutable zkEVM has instant finality, so a single block confirmation is fine. - uint16 internal constant MIN_CONFIRMATIONS = 1; - // We only need one word, and can expand that word in this system of contracts. - uint32 internal constant NUM_WORDS = 1; - - // The values returned by the VRF. - mapping(uint256 _fulfilmentId => bytes32 randomValue) private randomOutput; - - // VRF contract. - address public immutable vrfCoordinator; - - constructor(address _roleAdmin, address _configAdmin, address _vrfCoordinator) { - _grantRole(DEFAULT_ADMIN_ROLE, _roleAdmin); - _grantRole(CONFIG_ADMIN_ROLE, _configAdmin); - vrfCoordinator = _vrfCoordinator; - } - - /** - * @notice Callback called when random words are returned by the VRF. - * @dev Assumes external function that calls this checks that the random values are coming - * @dev from the VRF. - * @dev NOTE that Chainlink assumes that this function will not fail. - * @param _requestId is the fulfilment index. - * @param _randomWords are the random values from the VRF. - */ - function _fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal { - // NOTE: This function call is not allowed to fail. However, if one word is requested - // and some other number of words has been returned, then maybe the source has been - // compromised. Reverting the call is more likely to draw attention to the issue than - // emitting an event. - if (_randomWords.length != 1) { - revert UnexpectedRandomWordsLength(_randomWords.length); - } - randomOutput[_requestId] = bytes32(_randomWords[0]); - } - - /** - * @inheritdoc IOffchainRandomSource - */ - function getOffchainRandom( - uint256 _fulfilmentIndex - ) external view override(IOffchainRandomSource) returns (bytes32 _randomValue) { - bytes32 rand = randomOutput[_fulfilmentIndex]; - if (rand == bytes32(0)) { - revert WaitForRandom(); - } - _randomValue = rand; - } - - /** - * @inheritdoc IOffchainRandomSource - */ - function isOffchainRandomReady( - uint256 _fulfilmentIndex - ) external view override(IOffchainRandomSource) returns (bool) { - return randomOutput[_fulfilmentIndex] != bytes32(0); - } -} diff --git a/contracts/random/offchainsources/chainlink/ChainlinkSourceAdaptor.sol b/contracts/random/offchainsources/chainlink/ChainlinkSourceAdaptor.sol deleted file mode 100644 index 6d44445c..00000000 --- a/contracts/random/offchainsources/chainlink/ChainlinkSourceAdaptor.sol +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2 -pragma solidity 0.8.19; - -import {VRFConsumerBaseV2} from "./VRFConsumerBaseV2.sol"; -import {VRFCoordinatorV2Interface} from "./VRFCoordinatorV2Interface.sol"; -import {SourceAdaptorBase} from "../SourceAdaptorBase.sol"; -import {IOffchainRandomSource} from "../IOffchainRandomSource.sol"; - -/** - * @notice Fetch random numbers from the Chainlink Verifiable Random - * @notice Function (VRF). - * @dev This contract is NOT upgradeable. If there is an issue with this code, deploy a new - * version of the code and have the random seed provider point to the new version. - */ -contract ChainlinkSourceAdaptor is VRFConsumerBaseV2, SourceAdaptorBase { - /// @notice Log config changes. - event ConfigChanges(bytes32 _keyHash, uint64 _subId, uint32 _callbackGasLimit); - - /// @notice Relates to key that must sign the proof. - bytes32 public keyHash; - - /// @notice Subscription id. - uint64 public subId; - - /// @notice Gas limit when executing the callback. - uint32 public callbackGasLimit; - - /** - * @param _roleAdmin Admin that can add and remove config admins. - * @param _configAdmin Admin that can change the configuration. - * @param _vrfCoordinator VRF coordinator contract address. - * @param _keyHash Related to the signing / verification key. - * @param _subId Subscription id. - * @param _callbackGasLimit Gas limit to pass when calling the callback. - */ - constructor( - address _roleAdmin, - address _configAdmin, - address _vrfCoordinator, - bytes32 _keyHash, - uint64 _subId, - uint32 _callbackGasLimit - ) VRFConsumerBaseV2(_vrfCoordinator) SourceAdaptorBase(_roleAdmin, _configAdmin, _vrfCoordinator) { - keyHash = _keyHash; - subId = _subId; - callbackGasLimit = _callbackGasLimit; - } - - /** - * @notice Change the configuration. - * @param _keyHash Related to the signing / verification key. - * @param _subId Subscription id. - * @param _callbackGasLimit Gas limit to pass when calling the callback. - */ - function configureRequests( - bytes32 _keyHash, - uint64 _subId, - uint32 _callbackGasLimit - ) external onlyRole(CONFIG_ADMIN_ROLE) { - keyHash = _keyHash; - subId = _subId; - callbackGasLimit = _callbackGasLimit; - emit ConfigChanges(_keyHash, _subId, _callbackGasLimit); - } - - /** - * @inheritdoc IOffchainRandomSource - */ - function requestOffchainRandom() external override(IOffchainRandomSource) returns (uint256 _requestId) { - return - VRFCoordinatorV2Interface(vrfCoordinator).requestRandomWords( - keyHash, - subId, - MIN_CONFIRMATIONS, - callbackGasLimit, - NUM_WORDS - ); - } - - /** - * @inheritdoc VRFConsumerBaseV2 - */ - // solhint-disable-next-line private-vars-leading-underscore - function fulfillRandomWords( - uint256 _requestId, - uint256[] memory _randomWords - ) internal virtual override(VRFConsumerBaseV2) { - _fulfillRandomWords(_requestId, _randomWords); - } -} diff --git a/contracts/random/offchainsources/chainlink/VRFConsumerBaseV2.sol b/contracts/random/offchainsources/chainlink/VRFConsumerBaseV2.sol deleted file mode 100644 index 4c2a3b3e..00000000 --- a/contracts/random/offchainsources/chainlink/VRFConsumerBaseV2.sol +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -// Code from Chainlink's repo. -// This file has been copied here that than installing the chainlink contracts -// using the following command to dependency clashes. -// npm install @chainlink/contracts - -/** - * - * @notice Interface for contracts using VRF randomness - * ***************************************************************************** - * @dev PURPOSE - * - * @dev Reggie the Random Oracle (not his real job) wants to provide randomness - * @dev to Vera the verifier in such a way that Vera can be sure he's not - * @dev making his output up to suit himself. Reggie provides Vera a public key - * @dev to which he knows the secret key. Each time Vera provides a seed to - * @dev Reggie, he gives back a value which is computed completely - * @dev deterministically from the seed and the secret key. - * - * @dev Reggie provides a proof by which Vera can verify that the output was - * @dev correctly computed once Reggie tells it to her, but without that proof, - * @dev the output is indistinguishable to her from a uniform random sample - * @dev from the output space. - * - * @dev The purpose of this contract is to make it easy for unrelated contracts - * @dev to talk to Vera the verifier about the work Reggie is doing, to provide - * @dev simple access to a verifiable source of randomness. It ensures 2 things: - * @dev 1. The fulfilment came from the VRFCoordinator - * @dev 2. The consumer contract implements fulfillRandomWords. - * ***************************************************************************** - * @dev USAGE - * - * @dev Calling contracts must inherit from VRFConsumerBase, and can - * @dev initialize VRFConsumerBase's attributes in their constructor as - * @dev shown: - * - * @dev contract VRFConsumer { - * @dev constructor(, address _vrfCoordinator, address _link) - * @dev VRFConsumerBase(_vrfCoordinator) public { - * @dev - * @dev } - * @dev } - * - * @dev The oracle will have given you an ID for the VRF keypair they have - * @dev committed to (let's call it keyHash). Create subscription, fund it - * @dev and your consumer contract as a consumer of it (see VRFCoordinatorInterface - * @dev subscription management functions). - * @dev Call requestRandomWords(keyHash, subId, minimumRequestConfirmations, - * @dev callbackGasLimit, numWords), - * @dev see (VRFCoordinatorInterface for a description of the arguments). - * - * @dev Once the VRFCoordinator has received and validated the oracle's response - * @dev to your request, it will call your contract's fulfillRandomWords method. - * - * @dev The randomness argument to fulfillRandomWords is a set of random words - * @dev generated from your requestId and the blockHash of the request. - * - * @dev If your contract could have concurrent requests open, you can use the - * @dev requestId returned from requestRandomWords to track which response is associated - * @dev with which randomness request. - * @dev See "SECURITY CONSIDERATIONS" for principles to keep in mind, - * @dev if your contract could have multiple requests in flight simultaneously. - * - * @dev Colliding `requestId`s are cryptographically impossible as long as seeds - * @dev differ. - * - * ***************************************************************************** - * @dev SECURITY CONSIDERATIONS - * - * @dev A method with the ability to call your fulfillRandomness method directly - * @dev could spoof a VRF response with any random value, so it's critical that - * @dev it cannot be directly called by anything other than this base contract - * @dev (specifically, by the VRFConsumerBase.rawFulfillRandomness method). - * - * @dev For your users to trust that your contract's random behavior is free - * @dev from malicious interference, it's best if you can write it so that all - * @dev behaviors implied by a VRF response are executed *during* your - * @dev fulfillRandomness method. If your contract must store the response (or - * @dev anything derived from it) and use it later, you must ensure that any - * @dev user-significant behavior which depends on that stored value cannot be - * @dev manipulated by a subsequent VRF request. - * - * @dev Similarly, both miners and the VRF oracle itself have some influence - * @dev over the order in which VRF responses appear on the blockchain, so if - * @dev your contract could have multiple VRF requests in flight simultaneously, - * @dev you must ensure that the order in which the VRF responses arrive cannot - * @dev be used to manipulate your contract's user-significant behavior. - * - * @dev Since the block hash of the block which contains the requestRandomness - * @dev call is mixed into the input to the VRF *last*, a sufficiently powerful - * @dev miner could, in principle, fork the blockchain to evict the block - * @dev containing the request, forcing the request to be included in a - * @dev different block with a different hash, and therefore a different input - * @dev to the VRF. However, such an attack would incur a substantial economic - * @dev cost. This cost scales with the number of blocks the VRF oracle waits - * @dev until it calls responds to a request. It is for this reason that - * @dev that you can signal to an oracle you'd like them to wait longer before - * @dev responding to the request (however this is not enforced in the contract - * @dev and so remains effective only in the case of unmodified oracle software). - */ -abstract contract VRFConsumerBaseV2 { - error OnlyCoordinatorCanFulfill(address have, address want); - // solhint-disable-next-line chainlink-solidity/prefix-immutable-variables-with-i - - address private immutable vrfCoordinator; - - /** - * @param _vrfCoordinator address of VRFCoordinator contract - */ - constructor(address _vrfCoordinator) { - vrfCoordinator = _vrfCoordinator; - } - - /** - * @notice fulfillRandomness handles the VRF response. Your contract must - * @notice implement it. See "SECURITY CONSIDERATIONS" above for important - * @notice principles to keep in mind when implementing your fulfillRandomness - * @notice method. - * - * @dev VRFConsumerBaseV2 expects its subcontracts to have a method with this - * @dev signature, and will call it once it has verified the proof - * @dev associated with the randomness. (It is triggered via a call to - * @dev rawFulfillRandomness, below.) - * - * @param requestId The Id initially returned by requestRandomness - * @param randomWords the VRF output expanded to the requested number of words - */ - // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore - function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal virtual; - - // rawFulfillRandomness is called by VRFCoordinator when it receives a valid VRF - // proof. rawFulfillRandomness then calls fulfillRandomness, after validating - // the origin of the call - function rawFulfillRandomWords(uint256 requestId, uint256[] memory randomWords) external { - if (msg.sender != vrfCoordinator) { - revert OnlyCoordinatorCanFulfill(msg.sender, vrfCoordinator); - } - fulfillRandomWords(requestId, randomWords); - } -} diff --git a/contracts/random/offchainsources/chainlink/VRFCoordinatorV2Interface.sol b/contracts/random/offchainsources/chainlink/VRFCoordinatorV2Interface.sol deleted file mode 100644 index 4f93a76e..00000000 --- a/contracts/random/offchainsources/chainlink/VRFCoordinatorV2Interface.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -// Code from Chainlink's repo. This file is currently not installed when calling -// npm install @chainlink/contracts - -interface VRFCoordinatorV2Interface { - /** - * @notice Get configuration relevant for making requests - * @return minimumRequestConfirmations global min for request confirmations - * @return maxGasLimit global max for request gas limit - * @return s_provingKeyHashes list of registered key hashes - */ - function getRequestConfig() external view returns (uint16, uint32, bytes32[] memory); - - /** - * @notice Request a set of random words. - * @param keyHash - Corresponds to a particular oracle job which uses - * that key for generating the VRF proof. Different keyHash's have different gas price - * ceilings, so you can select a specific one to bound your maximum per request cost. - * @param subId - The ID of the VRF subscription. Must be funded - * with the minimum subscription balance required for the selected keyHash. - * @param minimumRequestConfirmations - How many blocks you'd like the - * oracle to wait before responding to the request. See SECURITY CONSIDERATIONS - * for why you may want to request more. The acceptable range is - * [minimumRequestBlockConfirmations, 200]. - * @param callbackGasLimit - How much gas you'd like to receive in your - * fulfillRandomWords callback. Note that gasleft() inside fulfillRandomWords - * may be slightly less than this amount because of gas used calling the function - * (argument decoding etc.), so you may need to request slightly more than you expect - * to have inside fulfillRandomWords. The acceptable range is - * [0, maxGasLimit] - * @param numWords - The number of uint256 random values you'd like to receive - * in your fulfillRandomWords callback. Note these numbers are expanded in a - * secure way by the VRFCoordinator from a single random value supplied by the oracle. - * @return requestId - A unique identifier of the request. Can be used to match - * a request to a response in fulfillRandomWords. - */ - function requestRandomWords( - bytes32 keyHash, - uint64 subId, - uint16 minimumRequestConfirmations, - uint32 callbackGasLimit, - uint32 numWords - ) external returns (uint256 requestId); - - /** - * @notice Create a VRF subscription. - * @return subId - A unique subscription id. - * @dev You can manage the consumer set dynamically with addConsumer/removeConsumer. - * @dev Note to fund the subscription, use transferAndCall. For example - * @dev LINKTOKEN.transferAndCall( - * @dev address(COORDINATOR), - * @dev amount, - * @dev abi.encode(subId)); - */ - function createSubscription() external returns (uint64 subId); - - /** - * @notice Get a VRF subscription. - * @param subId - ID of the subscription - * @return balance - LINK balance of the subscription in juels. - * @return reqCount - number of requests for this subscription, determines fee tier. - * @return owner - owner of the subscription. - * @return consumers - list of consumer address which are able to use this subscription. - */ - function getSubscription( - uint64 subId - ) external view returns (uint96 balance, uint64 reqCount, address owner, address[] memory consumers); - - /** - * @notice Request subscription owner transfer. - * @param subId - ID of the subscription - * @param newOwner - proposed new owner of the subscription - */ - function requestSubscriptionOwnerTransfer(uint64 subId, address newOwner) external; - - /** - * @notice Request subscription owner transfer. - * @param subId - ID of the subscription - * @dev will revert if original owner of subId has - * not requested that msg.sender become the new owner. - */ - function acceptSubscriptionOwnerTransfer(uint64 subId) external; - - /** - * @notice Add a consumer to a VRF subscription. - * @param subId - ID of the subscription - * @param consumer - New consumer which can use the subscription - */ - function addConsumer(uint64 subId, address consumer) external; - - /** - * @notice Remove a consumer from a VRF subscription. - * @param subId - ID of the subscription - * @param consumer - Consumer to remove from the subscription - */ - function removeConsumer(uint64 subId, address consumer) external; - - /** - * @notice Cancel a subscription - * @param subId - ID of the subscription - * @param to - Where to send the remaining LINK to - */ - function cancelSubscription(uint64 subId, address to) external; - - /* - * @notice Check to see if there exists a request commitment consumers - * for all consumers and keyhashes for a given sub. - * @param subId - ID of the subscription - * @return true if there exists at least one unfulfilled request for the subscription, false - * otherwise. - */ - function pendingRequestExists(uint64 subId) external view returns (bool); -} diff --git a/contracts/random/offchainsources/supra/ISupraRouter.sol b/contracts/random/offchainsources/supra/ISupraRouter.sol deleted file mode 100644 index bd2acdda..00000000 --- a/contracts/random/offchainsources/supra/ISupraRouter.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -/** - * @notice API for interacting with Supra's Verifiable Random Function. - */ -interface ISupraRouter { - function generateRequest( - string memory _functionSig, - uint8 _rngCount, - uint256 _numConfirmations, - uint256 _clientSeed, - address _clientWalletAddress - ) external returns (uint256); - function generateRequest( - string memory _functionSig, - uint8 _rngCount, - uint256 _numConfirmations, - address _clientWalletAddress - ) external returns (uint256); -} diff --git a/contracts/random/offchainsources/supra/SupraSourceAdaptor.sol b/contracts/random/offchainsources/supra/SupraSourceAdaptor.sol deleted file mode 100644 index 4da85674..00000000 --- a/contracts/random/offchainsources/supra/SupraSourceAdaptor.sol +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2023 -// SPDX-License-Identifier: Apache 2 -pragma solidity 0.8.19; - -import {ISupraRouter} from "./ISupraRouter.sol"; -import {SourceAdaptorBase} from "../SourceAdaptorBase.sol"; -import {IOffchainRandomSource} from "../IOffchainRandomSource.sol"; - -/** - * @notice Fetch random numbers from the Supra Verifiable Random - * @notice Function (VRF). - * @dev This contract is NOT upgradeable. If there is an issue with this code, deploy a new - * version of the code and have the random seed provider point to the new version. - */ -contract SupraSourceAdaptor is SourceAdaptorBase { - /// @notice Error if a contract other than the Supra Router attempts to supply random values. - error NotVrfContract(); - - /// @notice Subscription address has changed. - event SubscriptionChange(address _newSubscription); - - /// @notice Subscription access. - address public subscriptionAccount; - - /** - * @param _roleAdmin Admin that can add and remove config admins. - * @param _configAdmin Admin that can change the configuration. - * @param _vrfCoordinator VRF coordinator contract address. - * @param _subscription Subscription account. - */ - constructor( - address _roleAdmin, - address _configAdmin, - address _vrfCoordinator, - address _subscription - ) SourceAdaptorBase(_roleAdmin, _configAdmin, _vrfCoordinator) { - // slither-disable-next-line missing-zero-check - subscriptionAccount = _subscription; - } - - /** - * @notice Change the subscription account address. - * @param _subscription The address of the new subscription. - */ - function setSubscription(address _subscription) external onlyRole(CONFIG_ADMIN_ROLE) { - // slither-disable-next-line missing-zero-check - subscriptionAccount = _subscription; - emit SubscriptionChange(subscriptionAccount); - } - - /** - * @inheritdoc IOffchainRandomSource - */ - function requestOffchainRandom() external override(IOffchainRandomSource) returns (uint256 _requestId) { - return - ISupraRouter(vrfCoordinator).generateRequest( - "fulfillRandomWords(uint256,uint256[])", - uint8(NUM_WORDS), - MIN_CONFIRMATIONS, - subscriptionAccount - ); - } - - /** - * @notice Callback called when random words are returned by the VRF. - * @param _requestId is the fulfilment index. - * @param _randomWords are the random values from the VRF. - */ - function fulfillRandomWords(uint256 _requestId, uint256[] calldata _randomWords) external { - if (msg.sender != address(vrfCoordinator)) { - revert NotVrfContract(); - } - - _fulfillRandomWords(_requestId, _randomWords); - } -} diff --git a/contracts/random/random-architecture.png b/contracts/random/random-architecture.png deleted file mode 100644 index 33b0cca1..00000000 Binary files a/contracts/random/random-architecture.png and /dev/null differ diff --git a/contracts/random/random-sequence.png b/contracts/random/random-sequence.png deleted file mode 100644 index ab4033dc..00000000 Binary files a/contracts/random/random-sequence.png and /dev/null differ diff --git a/test/random/MockGame.sol b/test/random/MockGame.sol deleted file mode 100644 index c2069f36..00000000 --- a/test/random/MockGame.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import {RandomValues} from "contracts/random/RandomValues.sol"; - -contract MockGame is RandomValues { - constructor(address _randomSeedProvider) RandomValues(_randomSeedProvider) {} - - function requestRandomValueCreation(uint16 _size) external returns (uint256 _randomRequestId) { - return _requestRandomValueCreation(_size); - } - - function fetchRandomValues(uint256 _randomRequestId) external returns (bytes32[] memory _randomValues) { - return _fetchRandomValues(_randomRequestId); - } - - function isRandomValueReady(uint256 _randomRequestId) external view returns (RequestStatus) { - return _isRandomValueReady(_randomRequestId); - } -} diff --git a/test/random/MockOffchainSource.sol b/test/random/MockOffchainSource.sol deleted file mode 100644 index 47111a8a..00000000 --- a/test/random/MockOffchainSource.sol +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2024 -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import {IOffchainRandomSource} from "contracts/random/offchainsources/IOffchainRandomSource.sol"; - -contract MockOffchainSource is IOffchainRandomSource { - uint256 public nextIndex = 1000; - bool public isReady; - - function setIsReady(bool _ready) external { - isReady = _ready; - } - - function requestOffchainRandom() external override(IOffchainRandomSource) returns (uint256 _fulfilmentIndex) { - return nextIndex++; - } - - function getOffchainRandom(uint256 _fulfilmentIndex) - external - view - override(IOffchainRandomSource) - returns (bytes32 _randomValue) - { - if (!isReady) { - revert WaitForRandom(); - } - return keccak256(abi.encodePacked(_fulfilmentIndex)); - } - - function isOffchainRandomReady(uint256 /* _fulfilmentIndex */ ) external view returns (bool) { - return isReady; - } -} diff --git a/test/random/MockRandomSeedProviderV2.sol b/test/random/MockRandomSeedProviderV2.sol deleted file mode 100644 index 5a0e0512..00000000 --- a/test/random/MockRandomSeedProviderV2.sol +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Immutable Pty Ltd 2018 - 2024 -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import {RandomSeedProvider} from "contracts/random/RandomSeedProvider.sol"; - -contract MockRandomSeedProviderV2 is RandomSeedProvider { - uint256 internal constant VERSION2 = 2; - - function upgrade() external override(RandomSeedProvider) { - if (version == VERSION0) { - version = VERSION2; - } else { - revert CanNotUpgradeFrom(version, VERSION2); - } - } -} diff --git a/test/random/README.md b/test/random/README.md deleted file mode 100644 index 61a259c8..00000000 --- a/test/random/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# Test Plan for Random Number Generation contracts - -## RandomSeedProvider.sol -This section defines tests for contracts/random/RandomSeedProvider.sol. -All of these tests are in test/random/RandomSeedProvider.t.sol. - -Initialize testing: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testInit | Check that deployment + initialize work. | Yes | Yes | -| testReinit | Calling initialise a second time fails. | No | Yes | -| testGetRandomSeedInitTraditional | getRandomSeed(), initial value, method TRADITIONAL | Yes | Yes | -| testGetRandomSeedInitRandao | getRandomSeed(), initial value, method RANDAO | Yes | Yes | -| testGetRandomSeedNotGenTraditional | getRandomSeed(), when value not generated | No | Yes | -| testGetRandomSeedNotGenRandao | getRandomSeed(), when value not generated | No | Yes | -| testGetRandomSeedNoOffchainSource | getRandomSeed(), when no offchain source configured | No | Yes | - -Control functions tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testRoleAdmin | Check DEFAULT_ADMIN_ROLE can assign new roles. | Yes | Yes | -| testRoleAdminBadAuth | Check auth for create new admins. | No | Yes | -| testSetOffchainRandomSource | setOffchainRandomSource(). | Yes | Yes | -| testSetOffchainRandomSourceBadAuth | setOffchainRandomSource() without authorization. | No | Yes | -| testSetRanDaoAvailable | setRanDaoAvailable(). | Yes | Yes | -| testSetRanDaoAvailableBadAuth | setRanDaoAvailable() without authorization. | No | Yes | -| testAddOffchainRandomConsumer | addOffchainRandomConsumer(). | Yes | Yes | -| testAddOffchainRandomConsumerBadAuth | addOffchainRandomConsumer() without authorization.| No | Yes | -| testRemoveOffchainRandomConsumer| removeOffchainRandomConsumer(). | Yes | Yes | -| testRemoveOffchainRandomConsumerBadAuth | removeOffchainRandomConsumer() without authorization.| No | Yes | -| testUpgrade | Check that the contract can be upgraded. | Yes | Yes | -| testUpgradeBadAuth | Check upgrade authorisation. | No | Yes | -| testNoUpgrade | Upgrade from V0 to V0. | No | Yes | - - -Operational functions tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testTradNextBlock | Check basic request flow | Yes | Yes | -| testRanDaoNextBlock | Check basic request flow | Yes | Yes | -| testOffchainNextBlock | Check basic request flow | Yes | Yes | -| testOffchainNotReady | Attempt to fetch offchain random when not ready | No | Yes | -| testTradTwoInOneBlock | Two calls to requestRandomSeed in one block | Yes | Yes | -| testRanDaoTwoInOneBlock | Two calls to requestRandomSeed in one block | Yes | Yes | -| testOffchainTwoInOneBlock | Two calls to requestRandomSeed in one block | Yes | Yes | -| testTradDelayedFulfilment | Request then wait several blocks before fulfilment | Yes | Yes | -| testRanDaoDelayedFulfilment | Request then wait several blocks before fulfilment | Yes | Yes | - -Scenario: Generate some random numbers, switch random generation methodology, generate some more -numbers, check that the numbers generated earlier are still available: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testSwitchTraditionalOffchain | Traditional -> Off-chain. | Yes | Yes | -| testSwitchRandaoOffchain | RanDAO -> Off-chain. | Yes | Yes | -| testSwitchOffchainOffchain | Off-chain to another off-chain source. | Yes | Yes | -| testSwitchOffchainOnchain | Disable off-chain source. | Yes | Yes | - -## RandomValues.sol - -Initialize testing: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testInit | Check that contructor worked. | Yes | Yes | - - -Operational tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testNoValue | Request zero bytes be returned | No | Yes | -| testFirstValue | Return a single value | Yes | Yes | -| testSecondValue | Return two values | Yes | Yes | -| testMultiFetch | Attempt to fetch a generated number twice. | Yes | Yes | -| testFirstValues | Return a single set of values | Yes | Yes | -| testSecondValues | Return two sets of values | Yes | Yes | -| testMultipleGames | Multiple games in parallel. | Yes | Yes | - - - -## ChainlinkSource.sol -This section defines tests for contracts/random/offchainsources/chainlink/ChainlinkSource.sol. -All of these tests are in test/random/offchainsources/chainlink/ChainlinkSource.t.sol. - -Initialize testing: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testInit | Check that deployment and initialisation works. | Yes | Yes | - -Control functions tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testRoleAdmin | Check DEFAULT_ADMIN_ROLE can assign new roles. | Yes | Yes | -| testRoleAdminBadAuth | Check auth for create new admins. | No | Yes | -| testConfigureRequests | Check configureRequests can be called. | Yes | Yes | -| testConfigureRequestsBadAuth | Check configureRequests fails with bad auth. | No | Yes | - - -Operational functions tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testRequestRandom | Request a random value. | Yes | Yes | -| testTwoRequests | Check that two requests return different values. | Yes | Yes | -| testBadFulfilment | Return a set of random numbers rather than one. | No | Yes | -| testRequestTooEarly | Request before ready. | No | Yes | -| testHackFulfilment | Attempt to maliciously fulfil from other address. | No | Yes | - -Integration tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testEndToEnd | Request a random value from randomValues. | Yes | Yes | - - - -## SupraSource.sol -This section defines tests for contracts/random/offchainsources/supra/SupraSource.sol. -All of these tests are in test/random/offchainsources/supra/SupraSource.t.sol. - -Initialize testing: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testInit | Check that deployment and initialisation works. | Yes | Yes | - -Control functions tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testRoleAdmin | Check DEFAULT_ADMIN_ROLE can assign new roles. | Yes | Yes | -| testRoleAdminBadAuth | Check auth for create new admins. | No | Yes | -| testSetSubcription | Check setSubscription can be called. | Yes | Yes | -| testSetSubscriptionBadAuth | Check setSubscription fails with bad auth. | No | Yes | - - -Operational functions tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testRequestRandom | Request a random value. | Yes | Yes | -| testTwoRequests | Check that two requests return different values. | Yes | Yes | -| testBadFulfilment | Return a set of random numbers rather than one. | No | Yes | -| testRequestTooEarly | Request before ready. | No | Yes | -| testHackFulfilment | Attempt to maliciously fulfil from other address. | No | Yes | - -Integration tests: - -| Test name |Description | Happy Case | Implemented | -|---------------------------------| --------------------------------------------------|------------|-------------| -| testEndToEnd | Request a random value from randomValues. | Yes | Yes | diff --git a/test/random/RandomSeedProvider.t.sol b/test/random/RandomSeedProvider.t.sol deleted file mode 100644 index ef49b458..00000000 --- a/test/random/RandomSeedProvider.t.sol +++ /dev/null @@ -1,545 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import "forge-std/Test.sol"; - -import {MockOffchainSource} from "./MockOffchainSource.sol"; -import {MockRandomSeedProviderV2} from "./MockRandomSeedProviderV2.sol"; -import {RandomSeedProvider} from "contracts/random/RandomSeedProvider.sol"; -import {IOffchainRandomSource} from "contracts/random/offchainsources/IOffchainRandomSource.sol"; -import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -contract UninitializedRandomSeedProviderTest is Test { - error WaitForRandom(); - - event OffchainRandomSourceSet(address _offchainRandomSource); - event RanDaoEnabled(); - event OffchainRandomConsumerAdded(address _consumer); - event OffchainRandomConsumerRemoved(address _consumer); - - bytes32 public constant DEFAULT_ADMIN_ROLE = bytes32(0); - bytes32 public constant RANDOM_ADMIN_ROLE = keccak256("RANDOM_ADMIN_ROLE"); - bytes32 public constant UPGRADE_ADMIN_ROLE = bytes32("UPGRADE_ROLE"); - - address public constant ONCHAIN = address(1); - - ERC1967Proxy public proxy; - RandomSeedProvider public impl; - RandomSeedProvider public randomSeedProvider; - ERC1967Proxy public proxyRanDao; - RandomSeedProvider public randomSeedProviderRanDao; - - address public roleAdmin; - address public randomAdmin; - address public upgradeAdmin; - - function setUp() public virtual { - roleAdmin = makeAddr("roleAdmin"); - randomAdmin = makeAddr("randomAdmin"); - upgradeAdmin = makeAddr("upgradeAdmin"); - impl = new RandomSeedProvider(); - proxy = new ERC1967Proxy( - address(impl), - abi.encodeWithSelector(RandomSeedProvider.initialize.selector, roleAdmin, randomAdmin, upgradeAdmin, false) - ); - randomSeedProvider = RandomSeedProvider(address(proxy)); - - proxyRanDao = new ERC1967Proxy( - address(impl), - abi.encodeWithSelector(RandomSeedProvider.initialize.selector, roleAdmin, randomAdmin, upgradeAdmin, true) - ); - randomSeedProviderRanDao = RandomSeedProvider(address(proxyRanDao)); - - // Ensure we are on a new block number when we start the tests. In particular, don't - // be on the same block number as when the contracts were deployed. - vm.roll(block.number + 1); - } - - function testInit() public { - // This set-up mirrors what is in the setUp function. Have this code here - // so that the coverage tool picks up the use of the initialize function. - RandomSeedProvider impl1 = new RandomSeedProvider(); - ERC1967Proxy proxy1 = new ERC1967Proxy( - address(impl1), - abi.encodeWithSelector(RandomSeedProvider.initialize.selector, roleAdmin, randomAdmin, upgradeAdmin, false) - ); - RandomSeedProvider randomSeedProvider1 = RandomSeedProvider(address(proxy1)); - vm.roll(block.number + 1); - - // Check that the initialize funciton has worked correctly. - assertEq(randomSeedProvider1.nextRandomIndex(), 1, "nextRandomIndex"); - assertEq(randomSeedProvider1.lastBlockRandomGenerated(), block.number - 1, "lastBlockRandomGenerated"); - assertEq(randomSeedProvider1.randomSource(), ONCHAIN, "randomSource"); - assertFalse(randomSeedProvider1.ranDaoAvailable(), "RAN DAO should not be available"); - assertTrue(randomSeedProviderRanDao.ranDaoAvailable(), "RAN DAO should be available"); - - assertTrue(randomSeedProvider1.hasRole(DEFAULT_ADMIN_ROLE, roleAdmin)); - assertTrue(randomSeedProvider1.hasRole(RANDOM_ADMIN_ROLE, randomAdmin)); - assertTrue(randomSeedProvider1.hasRole(UPGRADE_ADMIN_ROLE, upgradeAdmin)); - } - - function testReinit() public { - vm.expectRevert(); - randomSeedProvider.initialize(roleAdmin, randomAdmin, upgradeAdmin, true); - } - - function testGetRandomSeedInitTraditional() public { - bytes32 seed = randomSeedProvider.getRandomSeed(0, ONCHAIN); - bytes32 expectedInitialSeed = keccak256(abi.encodePacked(block.chainid, blockhash(block.number - 2))); - assertEq(seed, expectedInitialSeed, "initial seed"); - } - - function testGetRandomSeedInitRandao() public { - bytes32 seed = randomSeedProviderRanDao.getRandomSeed(0, ONCHAIN); - bytes32 expectedInitialSeed = keccak256(abi.encodePacked(block.chainid, blockhash(block.number - 2))); - assertEq(seed, expectedInitialSeed, "initial seed"); - } - - function testGetRandomSeedNotGenTraditional() public { - vm.expectRevert(abi.encodeWithSelector(WaitForRandom.selector)); - randomSeedProvider.getRandomSeed(2, ONCHAIN); - } - - function testGetRandomSeedNotGenRandao() public { - vm.expectRevert(abi.encodeWithSelector(WaitForRandom.selector)); - randomSeedProviderRanDao.getRandomSeed(2, ONCHAIN); - } - - function testGetRandomSeedNoOffchainSource() public { - vm.expectRevert(); - randomSeedProvider.getRandomSeed(0, address(1000)); - } -} - -contract ControlRandomSeedProviderTest is UninitializedRandomSeedProviderTest { - error CanNotUpgradeFrom(uint256 _storageVersion, uint256 _codeVersion); - - event Upgraded(address indexed implementation); - - address public constant NEW_SOURCE = address(10001); - address public constant CONSUMER = address(10001); - - function testRoleAdmin() public { - bytes32 role = RANDOM_ADMIN_ROLE; - address newAdmin = makeAddr("newAdmin"); - - vm.prank(roleAdmin); - randomSeedProvider.grantRole(role, newAdmin); - assertTrue(randomSeedProvider.hasRole(role, newAdmin)); - } - - function testRoleAdminBadAuth() public { - bytes32 role = RANDOM_ADMIN_ROLE; - address newAdmin = makeAddr("newAdmin"); - vm.expectRevert(); - randomSeedProvider.grantRole(role, newAdmin); - } - - function testSetOffchainRandomSource() public { - vm.prank(randomAdmin); - vm.expectEmit(true, true, true, true); - emit OffchainRandomSourceSet(NEW_SOURCE); - randomSeedProvider.setOffchainRandomSource(NEW_SOURCE); - assertEq(randomSeedProvider.randomSource(), NEW_SOURCE); - } - - function testSetOffchainRandomSourceBadAuth() public { - vm.expectRevert(); - randomSeedProvider.setOffchainRandomSource(NEW_SOURCE); - } - - function testSetRanDaoAvailable() public { - assertEq(randomSeedProvider.ranDaoAvailable(), false); - vm.prank(randomAdmin); - vm.expectEmit(true, true, true, true); - emit RanDaoEnabled(); - randomSeedProvider.setRanDaoAvailable(); - assertEq(randomSeedProvider.ranDaoAvailable(), true); - } - - function testSetRanDaoAvailableBadAuth() public { - assertEq(randomSeedProvider.ranDaoAvailable(), false); - vm.expectRevert(); - randomSeedProvider.setRanDaoAvailable(); - assertEq(randomSeedProvider.ranDaoAvailable(), false); - } - - function testAddOffchainRandomConsumer() public { - assertEq(randomSeedProvider.approvedForOffchainRandom(CONSUMER), false); - vm.prank(randomAdmin); - vm.expectEmit(true, true, true, true); - emit OffchainRandomConsumerAdded(CONSUMER); - randomSeedProvider.addOffchainRandomConsumer(CONSUMER); - assertEq(randomSeedProvider.approvedForOffchainRandom(CONSUMER), true); - } - - function testAddOffchainRandomConsumerBadAuth() public { - vm.expectRevert(); - randomSeedProvider.addOffchainRandomConsumer(CONSUMER); - assertEq(randomSeedProvider.approvedForOffchainRandom(CONSUMER), false); - } - - function testRemoveOffchainRandomConsumer() public { - vm.prank(randomAdmin); - randomSeedProvider.addOffchainRandomConsumer(CONSUMER); - assertEq(randomSeedProvider.approvedForOffchainRandom(CONSUMER), true); - vm.prank(randomAdmin); - vm.expectEmit(true, true, true, true); - emit OffchainRandomConsumerRemoved(CONSUMER); - randomSeedProvider.removeOffchainRandomConsumer(CONSUMER); - assertEq(randomSeedProvider.approvedForOffchainRandom(CONSUMER), false); - } - - function testRemoveOffchainRandomConsumerBadAuth() public { - vm.prank(randomAdmin); - randomSeedProvider.addOffchainRandomConsumer(CONSUMER); - vm.expectRevert(); - randomSeedProvider.removeOffchainRandomConsumer(CONSUMER); - assertEq(randomSeedProvider.approvedForOffchainRandom(CONSUMER), true); - } - - function testUpgrade() public { - assertEq(randomSeedProvider.version(), 0); - - MockRandomSeedProviderV2 randomSeedProviderV2 = new MockRandomSeedProviderV2(); - - vm.prank(upgradeAdmin); - vm.expectEmit(true, true, true, true); - emit Upgraded(address(randomSeedProviderV2)); - randomSeedProvider.upgradeToAndCall( - address(randomSeedProviderV2), abi.encodeWithSelector(randomSeedProviderV2.upgrade.selector) - ); - assertEq(randomSeedProvider.version(), 2); - } - - function testUpgradeBadAuth() public { - MockRandomSeedProviderV2 randomSeedProviderV2 = new MockRandomSeedProviderV2(); - - vm.expectRevert(); - randomSeedProvider.upgradeToAndCall( - address(randomSeedProviderV2), abi.encodeWithSelector(randomSeedProviderV2.upgrade.selector) - ); - } - - function testNoUpgrade() public { - vm.prank(upgradeAdmin); - vm.expectRevert(abi.encodeWithSelector(CanNotUpgradeFrom.selector, 0, 0)); - randomSeedProvider.upgrade(); - } - - function testNoDowngrade() public { - MockRandomSeedProviderV2 randomSeedProviderV2 = new MockRandomSeedProviderV2(); - RandomSeedProvider randomSeedProviderV0 = new RandomSeedProvider(); - - vm.prank(upgradeAdmin); - randomSeedProvider.upgradeToAndCall( - address(randomSeedProviderV2), abi.encodeWithSelector(randomSeedProviderV2.upgrade.selector) - ); - - vm.prank(upgradeAdmin); - vm.expectRevert(abi.encodeWithSelector(CanNotUpgradeFrom.selector, 2, 0)); - randomSeedProvider.upgradeToAndCall( - address(randomSeedProviderV0), abi.encodeWithSelector(randomSeedProviderV0.upgrade.selector) - ); - } - - // Check that the downgrade code in MockRandomSeedProviderV2 works too. - function testV2NoDowngrade() public { - uint256 badVersion = 13; - uint256 versionStorageSlot = 308; - vm.store(address(randomSeedProvider), bytes32(versionStorageSlot), bytes32(badVersion)); - assertEq(randomSeedProvider.version(), badVersion); - - MockRandomSeedProviderV2 randomSeedProviderV2 = new MockRandomSeedProviderV2(); - - vm.prank(upgradeAdmin); - vm.expectRevert(abi.encodeWithSelector(CanNotUpgradeFrom.selector, badVersion, 2)); - randomSeedProvider.upgradeToAndCall( - address(randomSeedProviderV2), abi.encodeWithSelector(randomSeedProviderV2.upgrade.selector) - ); - } -} - -contract OperationalRandomSeedProviderTest is UninitializedRandomSeedProviderTest { - MockOffchainSource public offchainSource = new MockOffchainSource(); - - function testTradNextBlock() public { - (uint256 fulfilmentIndex, address source) = randomSeedProvider.requestRandomSeed(); - assertEq(source, ONCHAIN, "source"); - assertEq(fulfilmentIndex, 2, "index"); - - bool available = randomSeedProvider.isRandomSeedReady(fulfilmentIndex, source); - assertFalse(available, "Should not be ready yet"); - - vm.roll(block.number + 1); - - available = randomSeedProvider.isRandomSeedReady(fulfilmentIndex, source); - assertTrue(available, "Should be ready"); - - bytes32 seed = randomSeedProvider.getRandomSeed(fulfilmentIndex, source); - assertNotEq(seed, bytes32(0), "Should not be zero"); - } - - function testRanDaoNextBlock() public { - (uint256 fulfilmentIndex, address source) = randomSeedProviderRanDao.requestRandomSeed(); - assertEq(source, ONCHAIN, "source"); - assertEq(fulfilmentIndex, 2, "index"); - assertTrue(randomSeedProviderRanDao.ranDaoAvailable()); - - bool available = randomSeedProviderRanDao.isRandomSeedReady(fulfilmentIndex, source); - assertFalse(available, "Should not be ready yet"); - - vm.roll(block.number + 1); - - available = randomSeedProviderRanDao.isRandomSeedReady(fulfilmentIndex, source); - assertTrue(available, "Should be ready"); - - bytes32 seed = randomSeedProviderRanDao.getRandomSeed(fulfilmentIndex, source); - assertNotEq(seed, bytes32(0), "Should not be zero"); - } - - function testOffchainNextBlock() public { - vm.prank(randomAdmin); - randomSeedProvider.setOffchainRandomSource(address(offchainSource)); - - address aConsumer = makeAddr("aConsumer"); - vm.prank(randomAdmin); - randomSeedProvider.addOffchainRandomConsumer(aConsumer); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex, address source) = randomSeedProvider.requestRandomSeed(); - assertEq(source, address(offchainSource), "source"); - assertEq(fulfilmentIndex, 1000, "index"); - - bool available = randomSeedProvider.isRandomSeedReady(fulfilmentIndex, source); - assertFalse(available, "Should not be ready yet"); - - offchainSource.setIsReady(true); - - available = randomSeedProvider.isRandomSeedReady(fulfilmentIndex, source); - assertTrue(available, "Should be ready"); - - bytes32 seed = randomSeedProvider.getRandomSeed(fulfilmentIndex, source); - assertNotEq(seed, bytes32(0), "Should not be zero"); - } - - function testOffchainNotReady() public { - vm.prank(randomAdmin); - randomSeedProvider.setOffchainRandomSource(address(offchainSource)); - - address aConsumer = makeAddr("aConsumer"); - vm.prank(randomAdmin); - randomSeedProvider.addOffchainRandomConsumer(aConsumer); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex, address source) = randomSeedProvider.requestRandomSeed(); - - vm.expectRevert(abi.encodeWithSelector(WaitForRandom.selector)); - randomSeedProvider.getRandomSeed(fulfilmentIndex, source); - } - - function testTradTwoInOneBlock() public { - (uint256 randomRequestId1,) = randomSeedProvider.requestRandomSeed(); - (uint256 randomRequestId2,) = randomSeedProvider.requestRandomSeed(); - (uint256 randomRequestId3,) = randomSeedProvider.requestRandomSeed(); - assertEq(randomRequestId1, randomRequestId2, "Request id 1 and request id 2"); - assertEq(randomRequestId1, randomRequestId3, "Request id 1 and request id 3"); - } - - function testRanDaoTwoInOneBlock() public { - (uint256 randomRequestId1,) = randomSeedProviderRanDao.requestRandomSeed(); - (uint256 randomRequestId2,) = randomSeedProviderRanDao.requestRandomSeed(); - (uint256 randomRequestId3,) = randomSeedProviderRanDao.requestRandomSeed(); - assertEq(randomRequestId1, randomRequestId2, "Request id 1 and request id 2"); - assertEq(randomRequestId1, randomRequestId3, "Request id 1 and request id 3"); - } - - function testOffchainTwoInOneBlock() public { - vm.prank(randomAdmin); - randomSeedProvider.setOffchainRandomSource(address(offchainSource)); - - address aConsumer = makeAddr("aConsumer"); - vm.prank(randomAdmin); - randomSeedProvider.addOffchainRandomConsumer(aConsumer); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex1,) = randomSeedProvider.requestRandomSeed(); - vm.prank(aConsumer); - (uint256 fulfilmentIndex2,) = randomSeedProvider.requestRandomSeed(); - assertEq(fulfilmentIndex1, fulfilmentIndex2, "Request id 1 and request id 3"); - } - - function testTradDelayedFulfilment() public { - (uint256 randomRequestId1, address source1) = randomSeedProvider.requestRandomSeed(); - vm.roll(block.number + 1); - - (uint256 randomRequestId2, address source2) = randomSeedProvider.requestRandomSeed(); - bytes32 rand1a = randomSeedProvider.getRandomSeed(randomRequestId1, source1); - assertNotEq(rand1a, bytes32(0), "rand1a: Random Values is zero"); - (uint256 randomRequestId3,) = randomSeedProvider.requestRandomSeed(); - assertNotEq(randomRequestId1, randomRequestId2, "Request id 1 and request id 2"); - assertEq(randomRequestId2, randomRequestId3, "Request id 2 and request id 3"); - - vm.roll(block.number + 1); - bytes32 rand1b = randomSeedProvider.getRandomSeed(randomRequestId1, source1); - assertNotEq(rand1b, bytes32(0), "rand1b: Random Values is zero"); - { - bytes32 rand2 = randomSeedProvider.getRandomSeed(randomRequestId2, source2); - assertNotEq(rand2, bytes32(0), "rand2: Random Values is zero"); - assertNotEq(rand1a, rand2, "rand1a, rand2: Random Values equal"); - } - vm.roll(block.number + 1); - bytes32 rand1c = randomSeedProvider.getRandomSeed(randomRequestId1, source1); - assertNotEq(rand1c, bytes32(0), "rand1c: Random Values is zero"); - - assertEq(rand1a, rand1b, "rand1a, rand1b: Random Values not equal"); - assertEq(rand1a, rand1c, "rand1a, rand1c: Random Values not equal"); - } - - function testRanDaoDelayedFulfilment() public { - (uint256 randomRequestId1, address source1) = randomSeedProviderRanDao.requestRandomSeed(); - vm.roll(block.number + 1); - - (uint256 randomRequestId2, address source2) = randomSeedProviderRanDao.requestRandomSeed(); - bytes32 rand1a = randomSeedProviderRanDao.getRandomSeed(randomRequestId1, source1); - assertNotEq(rand1a, bytes32(0), "rand1a: Random Values is zero"); - (uint256 randomRequestId3,) = randomSeedProviderRanDao.requestRandomSeed(); - assertNotEq(randomRequestId1, randomRequestId2, "Request id 1 and request id 2"); - assertEq(randomRequestId2, randomRequestId3, "Request id 2 and request id 3"); - - vm.roll(block.number + 1); - bytes32 rand1b = randomSeedProviderRanDao.getRandomSeed(randomRequestId1, source1); - assertNotEq(rand1b, bytes32(0), "rand1b: Random Values is zero"); - { - bytes32 rand2 = randomSeedProviderRanDao.getRandomSeed(randomRequestId2, source2); - assertNotEq(rand2, bytes32(0), "rand2: Random Values is zero"); - assertNotEq(rand1a, rand2, "rand1a, rand2: Random Values equal"); - } - vm.roll(block.number + 1); - bytes32 rand1c = randomSeedProviderRanDao.getRandomSeed(randomRequestId1, source1); - assertNotEq(rand1c, bytes32(0), "rand1c: Random Values is zero"); - - assertEq(rand1a, rand1b, "rand1a, rand1b: Random Values not equal"); - assertEq(rand1a, rand1c, "rand1a, rand1c: Random Values not equal"); - } -} - -contract SwitchingRandomSeedProviderTest is UninitializedRandomSeedProviderTest { - MockOffchainSource public offchainSource = new MockOffchainSource(); - MockOffchainSource public offchainSource2 = new MockOffchainSource(); - - function testSwitchTraditionalOffchain() public { - address aConsumer = makeAddr("aConsumer"); - vm.prank(randomAdmin); - randomSeedProvider.addOffchainRandomConsumer(aConsumer); - - (uint256 fulfilmentIndex1, address source1) = randomSeedProvider.requestRandomSeed(); - assertEq(source1, ONCHAIN, "source"); - assertEq(fulfilmentIndex1, 2, "index"); - vm.roll(block.number + 1); - bytes32 seed1 = randomSeedProvider.getRandomSeed(fulfilmentIndex1, source1); - - vm.prank(randomAdmin); - randomSeedProvider.setOffchainRandomSource(address(offchainSource)); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex2, address source2) = randomSeedProvider.requestRandomSeed(); - assertEq(source2, address(offchainSource), "offchain source"); - assertEq(fulfilmentIndex2, 1000, "index"); - - offchainSource.setIsReady(true); - bytes32 seed2 = randomSeedProvider.getRandomSeed(fulfilmentIndex2, source2); - - bytes32 seed1a = randomSeedProvider.getRandomSeed(fulfilmentIndex1, source1); - - assertEq(seed1, seed1a, "Seed still available"); - assertNotEq(seed1, seed2, "Must be different"); - } - - function testSwitchRanDaoOffchain() public { - address aConsumer = makeAddr("aConsumer"); - vm.prank(randomAdmin); - randomSeedProviderRanDao.addOffchainRandomConsumer(aConsumer); - - (uint256 fulfilmentIndex1, address source1) = randomSeedProviderRanDao.requestRandomSeed(); - assertEq(source1, ONCHAIN, "source"); - assertEq(fulfilmentIndex1, 2, "index"); - vm.roll(block.number + 1); - bytes32 seed1 = randomSeedProviderRanDao.getRandomSeed(fulfilmentIndex1, source1); - - vm.prank(randomAdmin); - randomSeedProviderRanDao.setOffchainRandomSource(address(offchainSource)); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex2, address source2) = randomSeedProviderRanDao.requestRandomSeed(); - assertEq(source2, address(offchainSource), "offchain source"); - assertEq(fulfilmentIndex2, 1000, "index"); - - offchainSource.setIsReady(true); - bytes32 seed2 = randomSeedProviderRanDao.getRandomSeed(fulfilmentIndex2, source2); - - bytes32 seed1a = randomSeedProviderRanDao.getRandomSeed(fulfilmentIndex1, source1); - - assertEq(seed1, seed1a, "Seed still available"); - assertNotEq(seed1, seed2, "Must be different"); - } - - function testSwitchOffchainOffchain() public { - address aConsumer = makeAddr("aConsumer"); - vm.prank(randomAdmin); - randomSeedProviderRanDao.addOffchainRandomConsumer(aConsumer); - - vm.prank(randomAdmin); - randomSeedProviderRanDao.setOffchainRandomSource(address(offchainSource)); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex1, address source1) = randomSeedProviderRanDao.requestRandomSeed(); - assertEq(source1, address(offchainSource), "offchain source"); - assertEq(fulfilmentIndex1, 1000, "index"); - bool available = randomSeedProviderRanDao.isRandomSeedReady(fulfilmentIndex1, source1); - assertFalse(available, "Should not be ready1"); - - vm.prank(randomAdmin); - randomSeedProviderRanDao.setOffchainRandomSource(address(offchainSource2)); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex2, address source2) = randomSeedProviderRanDao.requestRandomSeed(); - assertEq(source2, address(offchainSource2), "offchain source 2"); - assertEq(fulfilmentIndex2, 1000, "index"); - - offchainSource.setIsReady(true); - available = randomSeedProviderRanDao.isRandomSeedReady(fulfilmentIndex1, source1); - assertTrue(available, "Should be ready"); - randomSeedProviderRanDao.getRandomSeed(fulfilmentIndex1, source1); - offchainSource2.setIsReady(true); - randomSeedProviderRanDao.getRandomSeed(fulfilmentIndex2, source2); - } - - function testSwitchOffchainOnchain() public { - address aConsumer = makeAddr("aConsumer"); - vm.prank(randomAdmin); - randomSeedProviderRanDao.addOffchainRandomConsumer(aConsumer); - - vm.prank(randomAdmin); - randomSeedProviderRanDao.setOffchainRandomSource(address(offchainSource)); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex1, address source1) = randomSeedProviderRanDao.requestRandomSeed(); - assertEq(source1, address(offchainSource), "offchain source"); - assertEq(fulfilmentIndex1, 1000, "index"); - - vm.prank(randomAdmin); - randomSeedProviderRanDao.setOffchainRandomSource(ONCHAIN); - - vm.prank(aConsumer); - (uint256 fulfilmentIndex2, address source2) = randomSeedProviderRanDao.requestRandomSeed(); - assertEq(source2, ONCHAIN, "on chain"); - assertEq(fulfilmentIndex2, 2, "index"); - - offchainSource.setIsReady(true); - randomSeedProviderRanDao.getRandomSeed(fulfilmentIndex1, source1); - - vm.roll(block.number + 1); - randomSeedProviderRanDao.getRandomSeed(fulfilmentIndex2, source2); - } -} diff --git a/test/random/RandomValues.t.sol b/test/random/RandomValues.t.sol deleted file mode 100644 index 62739b5a..00000000 --- a/test/random/RandomValues.t.sol +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import "forge-std/Test.sol"; - -import {MockGame, RandomValues} from "./MockGame.sol"; -import {RandomSeedProvider} from "contracts/random/RandomSeedProvider.sol"; -import {IOffchainRandomSource} from "contracts/random/offchainsources/IOffchainRandomSource.sol"; -import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -contract UninitializedRandomValuesTest is Test { - error RequestForNoRandomBytes(); - error RandomValuesPreviouslyFetched(); - error WaitForRandom(); - - address public constant ONCHAIN = address(0); - - ERC1967Proxy public proxy; - RandomSeedProvider public impl; - RandomSeedProvider public randomSeedProvider; - - MockGame public game1; - - address public roleAdmin; - address public randomAdmin; - address public upgradeAdmin; - - function setUp() public virtual { - roleAdmin = makeAddr("roleAdmin"); - randomAdmin = makeAddr("randomAdmin"); - upgradeAdmin = makeAddr("upgradeAdmin"); - - impl = new RandomSeedProvider(); - proxy = new ERC1967Proxy( - address(impl), - abi.encodeWithSelector(RandomSeedProvider.initialize.selector, roleAdmin, randomAdmin, upgradeAdmin, false) - ); - randomSeedProvider = RandomSeedProvider(address(proxy)); - - game1 = new MockGame(address(randomSeedProvider)); - - // Ensure we are on a new block number when we start the tests. In particular, don't - // be on the same block number as when the contracts were deployed. - vm.roll(block.number + 1); - } - - function testInit() public { - assertEq(address(game1.randomSeedProvider()), address(randomSeedProvider), "randomSeedProvider"); - assertEq( - uint256(game1.isRandomValueReady(0)), - uint256(RandomValues.RequestStatus.ALREADY_FETCHED), - "Should not be ready" - ); - } -} - -contract SingleGameRandomValuesTest is UninitializedRandomValuesTest { - uint16 public constant NUM_VALUES = 3; - - function testNoValues() public { - vm.expectRevert(abi.encodeWithSelector(RequestForNoRandomBytes.selector)); - game1.requestRandomValueCreation(0); - } - - function testFirstValue() public returns (bytes32) { - uint256 randomRequestId = game1.requestRandomValueCreation(1); - assertEq( - uint256(game1.isRandomValueReady(randomRequestId)), - uint256(RandomValues.RequestStatus.IN_PROGRESS), - "Ready in same block!" - ); - - vm.roll(block.number + 1); - assertEq( - uint256(game1.isRandomValueReady(randomRequestId)), - uint256(RandomValues.RequestStatus.READY), - "Should be ready by next block!" - ); - - bytes32[] memory randomValue = game1.fetchRandomValues(randomRequestId); - assertEq(randomValue.length, 1, "Random Values length"); - assertNotEq(randomValue[0], bytes32(0), "Random Value zero"); - - assertEq( - uint256(game1.isRandomValueReady(randomRequestId)), - uint256(RandomValues.RequestStatus.ALREADY_FETCHED), - "Should not be ready" - ); - return randomValue[0]; - } - - function testSecondValue() public { - bytes32 rand1 = testFirstValue(); - bytes32 rand2 = testFirstValue(); - assertNotEq(rand1, rand2, "Random Values equal"); - } - - function testMultiFetch() public { - uint256 randomRequestId1 = game1.requestRandomValueCreation(1); - vm.roll(block.number + 1); - game1.fetchRandomValues(randomRequestId1); - vm.roll(block.number + 1); - vm.expectRevert(abi.encodeWithSelector(RandomValuesPreviouslyFetched.selector)); - game1.fetchRandomValues(randomRequestId1); - } - - function testFirstValues() public { - uint256 randomRequestId = game1.requestRandomValueCreation(NUM_VALUES); - vm.roll(block.number + 1); - bytes32[] memory randomValues = game1.fetchRandomValues(randomRequestId); - assertEq(randomValues.length, NUM_VALUES, "wrong length"); - } - - function testSecondValues() public { - uint256 randomRequestId1 = game1.requestRandomValueCreation(NUM_VALUES); - uint256 randomRequestId2 = game1.requestRandomValueCreation(NUM_VALUES); - vm.roll(block.number + 1); - - bytes32[] memory randomValues1 = game1.fetchRandomValues(randomRequestId1); - bytes32[] memory randomValues2 = game1.fetchRandomValues(randomRequestId2); - - assertNotEq(randomValues1[0], randomValues2[0], "values1[0], values2[0]: Random Values equal"); - assertNotEq(randomValues1[1], randomValues2[1], "values1[1], values2[1]: Random Values equal"); - assertNotEq(randomValues1[2], randomValues2[2], "values1[2], values2[2]: Random Values equal"); - } - - function testMultipleGames() public { - MockGame game2 = new MockGame(address(randomSeedProvider)); - - uint256 randomRequestId1 = game1.requestRandomValueCreation(2); - uint256 randomRequestId2 = game2.requestRandomValueCreation(4); - assertEq( - uint256(game1.isRandomValueReady(randomRequestId1)), - uint256(RandomValues.RequestStatus.IN_PROGRESS), - "Ready in same block!" - ); - assertEq( - uint256(game2.isRandomValueReady(randomRequestId2)), - uint256(RandomValues.RequestStatus.IN_PROGRESS), - "Ready in same block!" - ); - - vm.roll(block.number + 1); - assertEq( - uint256(game1.isRandomValueReady(randomRequestId1)), uint256(RandomValues.RequestStatus.READY), "Ready!" - ); - assertEq( - uint256(game2.isRandomValueReady(randomRequestId2)), uint256(RandomValues.RequestStatus.READY), "Ready!" - ); - - bytes32[] memory randomValue1 = game1.fetchRandomValues(randomRequestId1); - bytes32[] memory randomValue2 = game2.fetchRandomValues(randomRequestId2); - assertEq(randomValue1.length, 2, "randomValue1 size"); - assertEq(randomValue2.length, 4, "randomValue2 size"); - } -} diff --git a/test/random/offchainsources/chainlink/ChainlinkSource.t.sol b/test/random/offchainsources/chainlink/ChainlinkSource.t.sol deleted file mode 100644 index b6ddb4d4..00000000 --- a/test/random/offchainsources/chainlink/ChainlinkSource.t.sol +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import "forge-std/Test.sol"; - -import {MockCoordinator} from "./MockCoordinator.sol"; -import {MockGame, RandomValues} from "../../MockGame.sol"; -import {RandomSeedProvider} from "contracts/random/RandomSeedProvider.sol"; -import {IOffchainRandomSource} from "contracts/random/offchainsources/IOffchainRandomSource.sol"; -import {ChainlinkSourceAdaptor} from "contracts/random/offchainsources/chainlink/ChainlinkSourceAdaptor.sol"; -import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -contract ChainlinkInitTests is Test { - event ConfigChanges(bytes32 _keyHash, uint64 _subId, uint32 _callbackGasLimit); - - bytes32 public constant CONFIG_ADMIN_ROLE = keccak256("CONFIG_ADMIN_ROLE"); - - bytes32 public constant KEY_HASH = bytes32(uint256(1)); - uint64 public constant SUB_ID = uint64(4); - uint32 public constant CALLBACK_GAS_LIMIT = uint32(200000); - - ERC1967Proxy public proxy; - RandomSeedProvider public impl; - RandomSeedProvider public randomSeedProvider; - - MockCoordinator public mockChainlinkCoordinator; - ChainlinkSourceAdaptor public chainlinkSourceAdaptor; - - address public roleAdmin; - address public randomAdmin; - address public configAdmin; - address public upgradeAdmin; - - function setUp() public virtual { - roleAdmin = makeAddr("roleAdmin"); - randomAdmin = makeAddr("randomAdmin"); - configAdmin = makeAddr("configAdmin"); - upgradeAdmin = makeAddr("upgradeAdmin"); - - impl = new RandomSeedProvider(); - proxy = new ERC1967Proxy( - address(impl), - abi.encodeWithSelector(RandomSeedProvider.initialize.selector, roleAdmin, randomAdmin, upgradeAdmin, false) - ); - randomSeedProvider = RandomSeedProvider(address(proxy)); - - mockChainlinkCoordinator = new MockCoordinator(); - chainlinkSourceAdaptor = new ChainlinkSourceAdaptor( - roleAdmin, configAdmin, address(mockChainlinkCoordinator), KEY_HASH, SUB_ID, CALLBACK_GAS_LIMIT - ); - mockChainlinkCoordinator.setAdaptor(address(chainlinkSourceAdaptor)); - - vm.prank(randomAdmin); - randomSeedProvider.setOffchainRandomSource(address(chainlinkSourceAdaptor)); - - // Ensure we are on a new block number when we start the tests. In particular, don't - // be on the same block number as when the contracts were deployed. - vm.roll(block.number + 1); - } - - function testInit() public { - mockChainlinkCoordinator = new MockCoordinator(); - chainlinkSourceAdaptor = new ChainlinkSourceAdaptor( - roleAdmin, configAdmin, address(mockChainlinkCoordinator), KEY_HASH, SUB_ID, CALLBACK_GAS_LIMIT - ); - - assertEq( - address(chainlinkSourceAdaptor.vrfCoordinator()), - address(mockChainlinkCoordinator), - "vrfCoord not set correctly" - ); - assertEq(chainlinkSourceAdaptor.keyHash(), KEY_HASH, "keyHash not set correctly"); - assertEq(chainlinkSourceAdaptor.subId(), SUB_ID, "subId not set correctly"); - assertEq(chainlinkSourceAdaptor.callbackGasLimit(), CALLBACK_GAS_LIMIT, "callbackGasLimit not set correctly"); - } -} - -contract ChainlinkControlTests is ChainlinkInitTests { - function testRoleAdmin() public { - bytes32 role = CONFIG_ADMIN_ROLE; - address newAdmin = makeAddr("newAdmin"); - - vm.prank(roleAdmin); - chainlinkSourceAdaptor.grantRole(role, newAdmin); - assertTrue(chainlinkSourceAdaptor.hasRole(role, newAdmin)); - } - - function testRoleAdminBadAuth() public { - bytes32 role = CONFIG_ADMIN_ROLE; - address newAdmin = makeAddr("newAdmin"); - vm.expectRevert(); - chainlinkSourceAdaptor.grantRole(role, newAdmin); - } - - function testConfigureRequests() public { - bytes32 keyHash = bytes32(uint256(2)); - uint64 subId = uint64(5); - uint32 callbackGasLimit = uint32(200001); - - vm.prank(configAdmin); - vm.expectEmit(true, true, true, true); - emit ConfigChanges(keyHash, subId, callbackGasLimit); - chainlinkSourceAdaptor.configureRequests(keyHash, subId, callbackGasLimit); - assertEq(chainlinkSourceAdaptor.keyHash(), keyHash, "keyHash not set correctly"); - assertEq(chainlinkSourceAdaptor.subId(), subId, "subId not set correctly"); - assertEq(chainlinkSourceAdaptor.callbackGasLimit(), callbackGasLimit, "callbackGasLimit not set correctly"); - } - - function testConfigureRequestsBadAuth() public { - bytes32 keyHash = bytes32(uint256(2)); - uint64 subId = uint64(5); - uint32 callbackGasLimit = uint32(200001); - - vm.expectRevert(); - chainlinkSourceAdaptor.configureRequests(keyHash, subId, callbackGasLimit); - } -} - -contract ChainlinkOperationalTests is ChainlinkInitTests { - error WaitForRandom(); - error UnexpectedRandomWordsLength(uint256 _length); - - event RequestId(uint256 _requestId); - - bytes32 public constant RAND1 = bytes32(uint256(0x1a)); - bytes32 public constant RAND2 = bytes32(uint256(0x1b)); - - function testRequestRandom() public { - vm.recordLogs(); - uint256 fulfilmentIndex = chainlinkSourceAdaptor.requestOffchainRandom(); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1); - assertEq(entries[0].topics[0], keccak256("RequestId(uint256)")); - uint256 requestId = abi.decode(entries[0].data, (uint256)); - assertEq(fulfilmentIndex, requestId, "Must be the same"); - - bool ready = chainlinkSourceAdaptor.isOffchainRandomReady(fulfilmentIndex); - assertFalse(ready, "Should not be ready yet"); - - mockChainlinkCoordinator.sendFulfill(fulfilmentIndex, uint256(RAND1)); - - ready = chainlinkSourceAdaptor.isOffchainRandomReady(fulfilmentIndex); - assertTrue(ready, "Should be ready"); - - bytes32 rand = chainlinkSourceAdaptor.getOffchainRandom(fulfilmentIndex); - assertEq(rand, RAND1, "Wrong value returned"); - } - - function testTwoRequests() public { - uint256 fulfilmentIndex1 = chainlinkSourceAdaptor.requestOffchainRandom(); - uint256 fulfilmentIndex2 = chainlinkSourceAdaptor.requestOffchainRandom(); - assertNotEq(fulfilmentIndex1, fulfilmentIndex2, "Different requests receive different indices"); - - bool ready = chainlinkSourceAdaptor.isOffchainRandomReady(fulfilmentIndex1); - assertFalse(ready, "Should not be ready yet1"); - ready = chainlinkSourceAdaptor.isOffchainRandomReady(fulfilmentIndex2); - assertFalse(ready, "Should not be ready yet2"); - - mockChainlinkCoordinator.sendFulfill(fulfilmentIndex2, uint256(RAND2)); - ready = chainlinkSourceAdaptor.isOffchainRandomReady(fulfilmentIndex1); - assertFalse(ready, "Should not be ready yet3"); - ready = chainlinkSourceAdaptor.isOffchainRandomReady(fulfilmentIndex2); - assertTrue(ready, "Should be ready1"); - - bytes32 rand = chainlinkSourceAdaptor.getOffchainRandom(fulfilmentIndex2); - assertEq(rand, RAND2, "Wrong value returned1"); - - mockChainlinkCoordinator.sendFulfill(fulfilmentIndex1, uint256(RAND1)); - ready = chainlinkSourceAdaptor.isOffchainRandomReady(fulfilmentIndex1); - assertTrue(ready, "Should be ready2"); - ready = chainlinkSourceAdaptor.isOffchainRandomReady(fulfilmentIndex2); - assertTrue(ready, "Should be ready3"); - - rand = chainlinkSourceAdaptor.getOffchainRandom(fulfilmentIndex1); - assertEq(rand, RAND1, "Wrong value returned2"); - } - - function testBadFulfilment() public { - uint256 fulfilmentIndex = chainlinkSourceAdaptor.requestOffchainRandom(); - - uint256 length = 2; - uint256[] memory randomWords = new uint256[](length); - randomWords[0] = uint256(RAND1); - - vm.expectRevert(abi.encodeWithSelector(UnexpectedRandomWordsLength.selector, length)); - mockChainlinkCoordinator.sendFulfillRaw(fulfilmentIndex, randomWords); - } - - function testRequestTooEarly() public { - uint256 fulfilmentIndex = chainlinkSourceAdaptor.requestOffchainRandom(); - - vm.expectRevert(abi.encodeWithSelector(WaitForRandom.selector)); - chainlinkSourceAdaptor.getOffchainRandom(fulfilmentIndex); - } - - function testHackFulfilment() public { - uint256 fulfilmentIndex = chainlinkSourceAdaptor.requestOffchainRandom(); - - MockCoordinator hackChainlinkCoordinator = new MockCoordinator(); - vm.expectRevert(); - hackChainlinkCoordinator.sendFulfill(fulfilmentIndex, uint256(RAND1)); - } -} - -contract ChainlinkIntegrationTests is ChainlinkOperationalTests { - function testEndToEnd() public { - MockGame game = new MockGame(address(randomSeedProvider)); - - vm.prank(randomAdmin); - randomSeedProvider.addOffchainRandomConsumer(address(game)); - - vm.recordLogs(); - uint256 randomRequestId = game.requestRandomValueCreation(1); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1, "Unexpected number of events emitted"); - assertEq(entries[0].topics[0], keccak256("RequestId(uint256)")); - uint256 fulfilmentIndex = abi.decode(entries[0].data, (uint256)); - - assertEq( - uint256(game.isRandomValueReady(randomRequestId)), - uint256(RandomValues.RequestStatus.IN_PROGRESS), - "Should not be ready yet" - ); - - mockChainlinkCoordinator.sendFulfill(fulfilmentIndex, uint256(RAND1)); - - assertEq( - uint256(game.isRandomValueReady(randomRequestId)), - uint256(RandomValues.RequestStatus.READY), - "Should be ready" - ); - - bytes32[] memory randomValue = game.fetchRandomValues(randomRequestId); - assertEq(randomValue.length, 1, "length"); - assertNotEq(randomValue[0], bytes32(0), "Random Value zero"); - } -} - -contract ChainlinkCoverageFakeTests is ChainlinkInitTests { - error OnlyCoordinatorCanFulfill(address have, address want); - - // Do calls to unused functions in MockCoordinator so that it doesn't impact the coverage results. - function testFixMockCoordinatorCoverage() public { - mockChainlinkCoordinator = new MockCoordinator(); - mockChainlinkCoordinator.setAdaptor(address(chainlinkSourceAdaptor)); - mockChainlinkCoordinator.getRequestConfig(); - uint64 subId = mockChainlinkCoordinator.createSubscription(); - mockChainlinkCoordinator.getSubscription(subId); - mockChainlinkCoordinator.requestSubscriptionOwnerTransfer(subId, address(0)); - mockChainlinkCoordinator.acceptSubscriptionOwnerTransfer(subId); - mockChainlinkCoordinator.addConsumer(subId, address(0)); - mockChainlinkCoordinator.removeConsumer(subId, address(0)); - mockChainlinkCoordinator.cancelSubscription(subId, address(0)); - mockChainlinkCoordinator.pendingRequestExists(subId); - } - - function testV2BaseChecksCoverage() public { - MockCoordinator mockChainlinkCoordinator2 = new MockCoordinator(); - mockChainlinkCoordinator2.setAdaptor(address(chainlinkSourceAdaptor)); - vm.expectRevert( - abi.encodeWithSelector( - OnlyCoordinatorCanFulfill.selector, - address(mockChainlinkCoordinator2), - address(mockChainlinkCoordinator) - ) - ); - mockChainlinkCoordinator2.sendFulfill(0, 0); - } -} diff --git a/test/random/offchainsources/chainlink/MockCoordinator.sol b/test/random/offchainsources/chainlink/MockCoordinator.sol deleted file mode 100644 index 6e7b7bf2..00000000 --- a/test/random/offchainsources/chainlink/MockCoordinator.sol +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import {VRFCoordinatorV2Interface} from - "../../../../contracts/random/offchainsources/chainlink/VRFCoordinatorV2Interface.sol"; -import {ChainlinkSourceAdaptor} from "../../../../contracts/random/offchainsources/chainlink/ChainlinkSourceAdaptor.sol"; - -contract MockCoordinator is VRFCoordinatorV2Interface { - event RequestId(uint256 _requestId); - - ChainlinkSourceAdaptor public adaptor; - uint256 public nextIndex = 1000; - - uint64 private subscriptionId = uint64(0); - bool private pending = false; - - function setAdaptor(address _adaptor) external { - adaptor = ChainlinkSourceAdaptor(_adaptor); - } - - function sendFulfill(uint256 _requestId, uint256 _rand) external { - uint256[] memory randomWords = new uint256[](1); - randomWords[0] = _rand; - adaptor.rawFulfillRandomWords(_requestId, randomWords); - } - - function sendFulfillRaw(uint256 _requestId, uint256[] calldata _rand) external { - adaptor.rawFulfillRandomWords(_requestId, _rand); - } - - function requestRandomWords(bytes32, uint64, uint16, uint32, uint32) external returns (uint256 requestId) { - requestId = nextIndex++; - emit RequestId(requestId); - } - - // Unused functions - - function getRequestConfig() external pure returns (uint16, uint32, bytes32[] memory) { - bytes32[] memory a; - return (uint16(0), uint32(0), a); - } - - function createSubscription() external view returns (uint64 subId) { - return subscriptionId; - } - - function getSubscription(uint64) - external - pure - returns (uint96 balance, uint64 reqCount, address owner, address[] memory consumers) - { - return (uint96(0), uint64(0), address(0), consumers); - } - - function requestSubscriptionOwnerTransfer(uint64, address) external {} - function acceptSubscriptionOwnerTransfer(uint64) external {} - function addConsumer(uint64, address) external {} - function removeConsumer(uint64, address) external {} - function cancelSubscription(uint64, address) external {} - - function pendingRequestExists(uint64) external view returns (bool) { - return pending; - } -} diff --git a/test/random/offchainsources/supra/MockSupraRouter.sol b/test/random/offchainsources/supra/MockSupraRouter.sol deleted file mode 100644 index cad61e49..00000000 --- a/test/random/offchainsources/supra/MockSupraRouter.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import {ISupraRouter} from "../../../../contracts/random/offchainsources/supra/ISupraRouter.sol"; -import {SupraSourceAdaptor} from "../../../../contracts/random/offchainsources/supra/SupraSourceAdaptor.sol"; - -contract MockSupraRouter is ISupraRouter { - event RequestId(uint256 _requestId); - - SupraSourceAdaptor public adaptor; - uint256 public nextIndex = 1000; - - uint64 private subscriptionId = uint64(0); - bool private pending = false; - - function setAdaptor(address _adaptor) external { - adaptor = SupraSourceAdaptor(_adaptor); - } - - function sendFulfill(uint256 _requestId, uint256 _rand) external { - uint256[] memory randomWords = new uint256[](1); - randomWords[0] = _rand; - adaptor.fulfillRandomWords(_requestId, randomWords); - } - - function sendFulfillRaw(uint256 _requestId, uint256[] calldata _rand) external { - adaptor.fulfillRandomWords(_requestId, _rand); - } - - function generateRequest( - string memory, /* _functionSig */ - uint8, /* _rngCount */ - uint256, /* _numConfirmations */ - address /* _clientWalletAddress */ - ) external returns (uint256 requestId) { - requestId = nextIndex++; - emit RequestId(requestId); - } - - // Unused functions - function generateRequest( - string memory, /* _functionSig */ - uint8, /* _rngCount */ - uint256, /* _numConfirmations */ - uint256, /* _clientSeed */ - address /* _clientWalletAddress */ - ) external returns (uint256 requestId) { - requestId = nextIndex++; - emit RequestId(requestId); - } -} diff --git a/test/random/offchainsources/supra/SupraSource.t.sol b/test/random/offchainsources/supra/SupraSource.t.sol deleted file mode 100644 index c7f75c8d..00000000 --- a/test/random/offchainsources/supra/SupraSource.t.sol +++ /dev/null @@ -1,240 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import "forge-std/Test.sol"; - -import {MockSupraRouter} from "./MockSupraRouter.sol"; -import {MockGame, RandomValues} from "../../MockGame.sol"; -import {RandomSeedProvider} from "contracts/random/RandomSeedProvider.sol"; -import {IOffchainRandomSource} from "contracts/random/offchainsources/IOffchainRandomSource.sol"; -import {SupraSourceAdaptor} from "contracts/random/offchainsources/supra/SupraSourceAdaptor.sol"; -import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -contract SupraInitTests is Test { - error NotVrfContract(); - - bytes32 public constant CONFIG_ADMIN_ROLE = keccak256("CONFIG_ADMIN_ROLE"); - - ERC1967Proxy public proxy; - RandomSeedProvider public impl; - RandomSeedProvider public randomSeedProvider; - - MockSupraRouter public mockSupraRouter; - SupraSourceAdaptor public supraSourceAdaptor; - - address public roleAdmin; - address public randomAdmin; - address public configAdmin; - address public upgradeAdmin; - - address public subscription = address(0x123); - - function setUp() public virtual { - roleAdmin = makeAddr("roleAdmin"); - randomAdmin = makeAddr("randomAdmin"); - configAdmin = makeAddr("configAdmin"); - upgradeAdmin = makeAddr("upgradeAdmin"); - - impl = new RandomSeedProvider(); - proxy = new ERC1967Proxy( - address(impl), - abi.encodeWithSelector(RandomSeedProvider.initialize.selector, roleAdmin, randomAdmin, upgradeAdmin, false) - ); - randomSeedProvider = RandomSeedProvider(address(proxy)); - - mockSupraRouter = new MockSupraRouter(); - supraSourceAdaptor = new SupraSourceAdaptor(roleAdmin, configAdmin, address(mockSupraRouter), subscription); - mockSupraRouter.setAdaptor(address(supraSourceAdaptor)); - - vm.prank(randomAdmin); - randomSeedProvider.setOffchainRandomSource(address(supraSourceAdaptor)); - - // Ensure we are on a new block number when we start the tests. In particular, don't - // be on the same block number as when the contracts were deployed. - vm.roll(block.number + 1); - } - - function testInit() public { - mockSupraRouter = new MockSupraRouter(); - supraSourceAdaptor = new SupraSourceAdaptor(roleAdmin, configAdmin, address(mockSupraRouter), subscription); - - assertEq(address(supraSourceAdaptor.vrfCoordinator()), address(mockSupraRouter), "vrfCoord not set correctly"); - assertEq(supraSourceAdaptor.subscriptionAccount(), subscription, "Subscription account did not match"); - assertTrue(supraSourceAdaptor.hasRole(CONFIG_ADMIN_ROLE, configAdmin), "Role config admin"); - } -} - -contract SupraControlTests is SupraInitTests { - event SubscriptionChange(address _newSubscription); - - function testRoleAdmin() public { - bytes32 role = CONFIG_ADMIN_ROLE; - address newAdmin = makeAddr("newAdmin"); - - vm.prank(roleAdmin); - supraSourceAdaptor.grantRole(role, newAdmin); - assertTrue(supraSourceAdaptor.hasRole(role, newAdmin)); - } - - function testRoleAdminBadAuth() public { - bytes32 role = CONFIG_ADMIN_ROLE; - address newAdmin = makeAddr("newAdmin"); - vm.expectRevert(); - supraSourceAdaptor.grantRole(role, newAdmin); - } - - function testSetSubscription() public { - address newSub = address(7); - - vm.prank(configAdmin); - vm.expectEmit(true, true, true, true); - emit SubscriptionChange(newSub); - supraSourceAdaptor.setSubscription(newSub); - assertEq(supraSourceAdaptor.subscriptionAccount(), newSub, "subscription not set correctly"); - } - - function testSetSubscriptionBadAuth() public { - address newSub = address(7); - - vm.expectRevert(); - supraSourceAdaptor.setSubscription(newSub); - } -} - -contract SupraOperationalTests is SupraInitTests { - error WaitForRandom(); - error UnexpectedRandomWordsLength(uint256 _length); - - event RequestId(uint256 _requestId); - - bytes32 public constant RAND1 = bytes32(uint256(0x1a)); - bytes32 public constant RAND2 = bytes32(uint256(0x1b)); - - function testRequestRandom() public { - vm.recordLogs(); - uint256 fulfilmentIndex = supraSourceAdaptor.requestOffchainRandom(); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1); - assertEq(entries[0].topics[0], keccak256("RequestId(uint256)")); - uint256 requestId = abi.decode(entries[0].data, (uint256)); - assertEq(fulfilmentIndex, requestId, "Must be the same"); - - bool ready = supraSourceAdaptor.isOffchainRandomReady(fulfilmentIndex); - assertFalse(ready, "Should not be ready yet"); - - mockSupraRouter.sendFulfill(fulfilmentIndex, uint256(RAND1)); - - ready = supraSourceAdaptor.isOffchainRandomReady(fulfilmentIndex); - assertTrue(ready, "Should be ready"); - - bytes32 rand = supraSourceAdaptor.getOffchainRandom(fulfilmentIndex); - assertEq(rand, RAND1, "Wrong value returned"); - } - - function testTwoRequests() public { - uint256 fulfilmentIndex1 = supraSourceAdaptor.requestOffchainRandom(); - uint256 fulfilmentIndex2 = supraSourceAdaptor.requestOffchainRandom(); - assertNotEq(fulfilmentIndex1, fulfilmentIndex2, "Different requests receive different indices"); - - bool ready = supraSourceAdaptor.isOffchainRandomReady(fulfilmentIndex1); - assertFalse(ready, "Should not be ready yet1"); - ready = supraSourceAdaptor.isOffchainRandomReady(fulfilmentIndex2); - assertFalse(ready, "Should not be ready yet2"); - - mockSupraRouter.sendFulfill(fulfilmentIndex2, uint256(RAND2)); - ready = supraSourceAdaptor.isOffchainRandomReady(fulfilmentIndex1); - assertFalse(ready, "Should not be ready yet3"); - ready = supraSourceAdaptor.isOffchainRandomReady(fulfilmentIndex2); - assertTrue(ready, "Should be ready1"); - - bytes32 rand = supraSourceAdaptor.getOffchainRandom(fulfilmentIndex2); - assertEq(rand, RAND2, "Wrong value returned1"); - - mockSupraRouter.sendFulfill(fulfilmentIndex1, uint256(RAND1)); - ready = supraSourceAdaptor.isOffchainRandomReady(fulfilmentIndex1); - assertTrue(ready, "Should be ready2"); - ready = supraSourceAdaptor.isOffchainRandomReady(fulfilmentIndex2); - assertTrue(ready, "Should be ready3"); - - rand = supraSourceAdaptor.getOffchainRandom(fulfilmentIndex1); - assertEq(rand, RAND1, "Wrong value returned2"); - } - - function testBadFulfilment() public { - uint256 fulfilmentIndex = supraSourceAdaptor.requestOffchainRandom(); - - uint256 length = 2; - uint256[] memory randomWords = new uint256[](length); - randomWords[0] = uint256(RAND1); - - vm.expectRevert(abi.encodeWithSelector(UnexpectedRandomWordsLength.selector, length)); - mockSupraRouter.sendFulfillRaw(fulfilmentIndex, randomWords); - } - - function testRequestTooEarly() public { - uint256 fulfilmentIndex = supraSourceAdaptor.requestOffchainRandom(); - - vm.expectRevert(abi.encodeWithSelector(WaitForRandom.selector)); - supraSourceAdaptor.getOffchainRandom(fulfilmentIndex); - } - - function testHackFulfilment() public { - uint256 fulfilmentIndex = supraSourceAdaptor.requestOffchainRandom(); - - MockSupraRouter hackSupraRouter = new MockSupraRouter(); - hackSupraRouter.setAdaptor(address(supraSourceAdaptor)); - - vm.expectRevert(abi.encodeWithSelector(NotVrfContract.selector)); - hackSupraRouter.sendFulfill(fulfilmentIndex, uint256(RAND1)); - } -} - -contract SupraIntegrationTests is SupraOperationalTests { - function testEndToEnd() public { - MockGame game = new MockGame(address(randomSeedProvider)); - - vm.prank(randomAdmin); - randomSeedProvider.addOffchainRandomConsumer(address(game)); - - vm.recordLogs(); - uint256 randomRequestId = game.requestRandomValueCreation(1); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1, "Unexpected number of events emitted"); - assertEq(entries[0].topics[0], keccak256("RequestId(uint256)")); - uint256 fulfilmentIndex = abi.decode(entries[0].data, (uint256)); - - assertEq( - uint256(game.isRandomValueReady(randomRequestId)), - uint256(RandomValues.RequestStatus.IN_PROGRESS), - "Should not be ready yet" - ); - - mockSupraRouter.sendFulfill(fulfilmentIndex, uint256(RAND1)); - - assertEq( - uint256(game.isRandomValueReady(randomRequestId)), - uint256(RandomValues.RequestStatus.READY), - "Should be ready" - ); - - bytes32[] memory randomValue = game.fetchRandomValues(randomRequestId); - assertEq(randomValue.length, 1, "length"); - assertNotEq(randomValue[0], bytes32(0), "Random Value zero"); - } -} - -contract SupraCoverageFakeTests is SupraInitTests { - // Do calls to unused functions in MockSupraRouter so that it doesn't impact the coverage results. - function testFixMockCoordinatorCoverage() public { - mockSupraRouter = new MockSupraRouter(); - mockSupraRouter.setAdaptor(address(supraSourceAdaptor)); - string memory str = ""; - mockSupraRouter.generateRequest( - str, - uint8(0), /* _rngCount */ - uint256(0), /* _numConfirmations */ - uint256(0), /* _clientSeed */ - address(0) /* _clientWalletAddress */ - ); - } -}