// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IArbSys { function arbBlockNumber() external view returns (uint256); } /// @title SeedSource — the committed-future-block randomness shared by MegaJackpot and MegaDraw /// @notice A round commits to a block number that does not exist yet. The seed is the keccak of the /// hashes of THREE blocks starting there, `seedStep` apart, so that no single transaction /// landing in one block decides it. /// /// On an ordinary EVM chain: `block.number` and `blockhash()` (last 256 blocks). /// On an Arbitrum-technology chain such as Robinhood Chain (`arbitrum = true`): `block.number` /// is only an estimate of the parent chain's height and `blockhash()` is an ArbOS table nobody /// can query from an RPC, so we use the real L2 block number (ArbSys) and the EIP-2935 history /// contract, which serves real L2 block hashes for the last 393,168 L2 blocks (~11 hours on /// Robinhood Chain). Those hashes are what `eth_getBlockByNumber` returns, so a verifier needs /// nothing but the public RPC. /// /// Only whoever produces blocks could influence a block hash; on Robinhood Chain that is the single /// sequencer, which builds all three blocks, so three blocks do not add protection against it. No /// holder, keeper or team member can choose a hash. There is no VRF on the chain today. abstract contract SeedSource { address internal constant ARBSYS = 0x0000000000000000000000000000000000000064; address internal constant HISTORY = 0x0000F90827F1C53a10cb7A02335B175320002935; // EIP-2935 bool public immutable arbitrum; uint64 public immutable seedDelay; // blocks between the commitment and the first seed block uint64 public immutable seedStep; // blocks between the three seed blocks error BadDelay(); constructor(bool arbitrum_, uint64 seedDelay_, uint64 seedStep_) { if (seedDelay_ == 0 || seedDelay_ > 10_000 || seedStep_ == 0 || seedStep_ > 1_000) revert BadDelay(); arbitrum = arbitrum_; seedDelay = seedDelay_; seedStep = seedStep_; } /// The block number the rounds are measured in (the L2 height on Arbitrum-technology chains). function currentBlock() public view returns (uint256) { return arbitrum ? IArbSys(ARBSYS).arbBlockNumber() : block.number; } /// The first block after which draw() can run for a commitment at `seedBlock`. function seedReadyAt(uint256 seedBlock) public view returns (uint256) { return seedBlock + 2 * seedStep; } /// The combined seed for a commitment, or 0 while any of its three blocks is unavailable (not yet /// mined, or too old for the chain's hash window). function seedHashFor(uint256 seedBlock) public view returns (bytes32) { bytes32 a = _hashOf(seedBlock); bytes32 b = _hashOf(seedBlock + seedStep); bytes32 c = _hashOf(seedBlock + 2 * seedStep); if (a == bytes32(0) || b == bytes32(0) || c == bytes32(0)) return bytes32(0); return keccak256(abi.encode(a, b, c)); } /// Whether the first seed block has certainly left the chain's hash window: 256 blocks for blockhash(), /// and past the 393,168 L2 blocks EIP-2935 serves on Arbitrum (with a margin). Only then may a round /// give up on its seed, so a hash that is merely unreadable in one call (low gas) can never change an outcome. function seedExpired(uint256 seedBlock) public view returns (bool) { return currentBlock() > seedBlock + (arbitrum ? 400_000 : 256); } function _hashOf(uint256 n) internal view virtual returns (bytes32) { if (!arbitrum) return blockhash(n); (bool ok, bytes memory data) = HISTORY.staticcall(abi.encode(n)); return ok && data.length >= 32 ? abi.decode(data, (bytes32)) : bytes32(0); } }