Oracle RNG (oRNG)

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/*──────────────────────────────────────────────────────────────────────────────
│ ERC1967Proxy - Transparent Upgradeable Proxy (minimal, hardened)
│
│ IMPORTANT OPERATIONAL GUIDELINES
│ - ALWAYS pass the encoded initializer (`_data`) for the implementation when
│   deploying the proxy. This version enforces it at runtime.
│ - The proxy "admin" is the upgrade authority ONLY. Admin cannot use app
│   functions through the proxy (by design). Use a separate EOA/account for UX.
│ - Recommended: make the admin a multisig.
│
│ Pattern: Transparent Proxy (EIP-1967 storage slots)
│ The implementation contract (e.g., OracleRNG) should expose an `initialize()`
│ function instead of a constructor so state can be set via `_data`.
│
│ This proxy restricts `fallback` when `msg.sender == admin` to prevent the
│ admin from accidentally interacting with the app.
└──────────────────────────────────────────────────────────────────────────────*/

contract ERC1967Proxy {
    // EIP-1967 slots (implementation = keccak256("eip1967.proxy.implementation")-1, admin likewise)
    bytes32 private constant IMPLEMENTATION_SLOT =
        0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 private constant ADMIN_SLOT =
        0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

    event Upgraded(address indexed implementation);
    event AdminChanged(address indexed previousAdmin, address indexed newAdmin);

    /**
     * @param _logic  The address of the initial implementation
     * @param _admin  The admin (upgrade authority). Use a multisig!
     * @param _data   Call data to initialize the implementation's storage
     */
    constructor(address _logic, address _admin, bytes memory _data) payable {
        require(_isContract(_logic), "ERC1967Proxy: logic not contract");
        require(_admin != address(0), "ERC1967Proxy: zero admin");
        require(_data.length > 0, "ERC1967Proxy: initializer required");
        _setAdmin(_admin);
        _setImplementation(_logic);
        // delegatecall initializer into implementation
        (bool success, bytes memory returndata) = _logic.delegatecall(_data);
        require(success, string(returndata));
    }

    modifier ifAdmin() {
        if (msg.sender == _getAdmin()) {
            _;
        } else {
            _fallback();
        }
    }

    /* ───────────────────────── Admin Interface (only as admin) ───────────────────────── */

    function upgradeTo(address newImplementation) external ifAdmin {
        require(_isContract(newImplementation), "ERC1967Proxy: not contract");
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }

    function upgradeToAndCall(address newImplementation, bytes calldata data)
        external
        payable
        ifAdmin
    {
        require(_isContract(newImplementation), "ERC1967Proxy: not contract");
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
        (bool success, bytes memory returndata) = newImplementation.delegatecall(data);
        require(success, string(returndata));
    }

    function changeAdmin(address newAdmin) external ifAdmin {
        require(newAdmin != address(0), "ERC1967Proxy: zero admin");
        address oldAdmin = _getAdmin();
        _setAdmin(newAdmin);
        emit AdminChanged(oldAdmin, newAdmin);
    }

    /// @notice Viewable only by admin to avoid accidental UX misuse
    function admin() external ifAdmin returns (address) {
        return _getAdmin();
    }

    /// @notice Viewable only by admin to avoid accidental UX misuse
    function implementation() external ifAdmin returns (address) {
        return _getImplementation();
    }

    /* ────────────────────────────── Fallback/Receive ────────────────────────────── */

    fallback() external payable {
        _fallback();
    }

    receive() external payable {
        _fallback();
    }

    function _fallback() internal {
        // Admin is blocked from application calls through the proxy by design
        require(msg.sender != _getAdmin(), "ERC1967Proxy: admin cannot fallback");
        _delegate(_getImplementation());
    }

    function _delegate(address impl) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
                case 0 { revert(0, returndatasize()) }
                default { return(0, returndatasize()) }
        }
    }

    /* ───────────────────────────── Internal slot helpers ───────────────────────────── */

    function _getAdmin() private view returns (address adm) {
        bytes32 slot = ADMIN_SLOT;
        assembly { adm := sload(slot) }
    }

    function _setAdmin(address newAdmin) private {
        bytes32 slot = ADMIN_SLOT;
        assembly { sstore(slot, newAdmin) }
    }

    function _getImplementation() private view returns (address impl) {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly { impl := sload(slot) }
    }

    function _setImplementation(address newImplementation) private {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly { sstore(slot, newImplementation) }
    }

    function _isContract(address account) private view returns (bool) {
        return account.code.length > 0;
    }
}

/*──────────────────────────────────────────────────────────────────────────────
│ IOracleRNGConsumer - Callback interface for RNG consumers
└──────────────────────────────────────────────────────────────────────────────*/
interface IOracleRNGConsumer {
    function fulfillRandomness(uint256 requestId, uint256 randomness) external;
}

/*──────────────────────────────────────────────────────────────────────────────
│ OracleRNG - Minimal Randomness Oracle + PvP example (Upgradeable via Proxy)
│
│ SECURITY NOTE
│ - Uses blockhash-based entropy. Good for low-stakes; NOT for high-stakes.
│ - For valuable decisions, prefer VRF or commit-reveal (upgradeable later).
│
│ PATCHES IMPLEMENTED (relative to the original):
│  1) RNG finalization can proceed with ZERO participants (no rewards then).
│  2) Finalization requires block.number > deadline (off-by-one fixed).
│  3) Added owner() getter; initialize() must be called exactly once via proxy.
│  4) requestRngWhitelisted validates array size before casting to uint16.
│  5) PvP auto-joins P1/P2 into the RNG (resilience) without exceeding caps.
│  6) Escape hatches: cancelOpenBattle() and cancelStuckBattle() to avoid
│     stranded escrow (expired RNG or never-joined scenarios).
│  7) Enforced non-zero owner in initialize() to avoid proxy-bricking.
│  8) Minor gas polish in loops + storage gap for safer upgrades.
└──────────────────────────────────────────────────────────────────────────────*/
contract OracleRNG {
    /* ─────────────── ERC20 Minimal ─────────────── */
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    /* ───────────── Ownership & Reentrancy ───────────── */
    address private _owner;
    bool private _initialized;
    uint256 private _entered; // 0 = not entered, 1 = entered

    modifier onlyOwner() {
        require(msg.sender == _owner, "Not owner");
        _;
    }

    modifier nonReentrant() {
        require(_entered == 0, "Reentrancy");
        _entered = 1;
        _;
        _entered = 0;
    }

    event OwnershipTransferred(address indexed from, address indexed to);

    /* ───────────── Oracle State ───────────── */
    uint256 public oracleFee;     // fee in wei per RNG request
    uint256 public nextRngId;
    uint256 public nextBattleId;

    struct RngRequest {
        address requester;
        uint64  deadline;          // block number after which finalization can start
        uint64  expiryBlock;       // block number after which finalization MUST fail (blockhash window)
        uint16  maxParticipants;   // cap to prevent unbounded gas in finalize
        uint16  participantCount;  // number of unique participants
        uint256 randomResult;      // final randomness (0 until finalized)
        address[] participants;    // list for reward splitting
        bool isWhitelisted;
        bool callbackOnFinalize;
        bool finalized;
        mapping(address => bool) whitelist; // only used when isWhitelisted == true
    }

    mapping(uint256 => RngRequest) private _rngRequests;
    mapping(uint256 => mapping(address => bool)) public hasJoined;

    /* ───────────── PvP State ───────────── */
    enum BattleState { None, WaitingP2, WaitingRng, Resolved }

    struct Battle {
        address player1;
        address player2;
        uint256 wager;      // tokens escrowed (oRNG)
        uint256 rngId;      // RNG used to decide winner
        BattleState state;
    }

    mapping(uint256 => Battle) private _battles;
    mapping(uint256 => uint256) private _battleEscrow;

    /* ───────────── Events ───────────── */
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    event RngRequested(uint256 indexed id, address indexed requester);
    event RngJoined(uint256 indexed id, address indexed participant);
    event RngFinalized(uint256 indexed id, uint256 randomness);
    event BattleCreated(uint256 indexed id, address indexed player1, uint256 wager);
    event BattleMatched(uint256 indexed id, address indexed player2);
    event BattleResolved(uint256 indexed id, address indexed winner, uint256 prize);
    event OracleFeeUpdated(uint256 oldFee, uint256 newFee);
    event FeeWithdrawn(address indexed to, uint256 amount);

    /* ───────────── Initialization ───────────── */

    /**
     * @notice Must be invoked via proxy constructor `_data` on deployment.
     * @param owner_ Owner address (recommended: multisig). MUST be non-zero.
     * @param fee    Fee per RNG request in wei (use 0.001 ether if zero provided).
     */
    function initialize(address owner_, uint256 fee) external {
        require(!_initialized, "Initialized");
        require(owner_ != address(0), "Owner required");
        _initialized = true;

        _owner = owner_;
        emit OwnershipTransferred(address(0), _owner);

        name = "Oracle RNG";
        symbol = "oRNG";
        decimals = 18;

        oracleFee   = fee == 0 ? 0.001 ether : fee;
        nextRngId   = 1;
        nextBattleId = 1;
    }

    /// @notice Standard getter (tooling/UI expect this to exist)
    function owner() public view returns (address) {
        return _owner;
    }

    /**
     * @notice Transfer ownership. Use a multisig to minimize risk.
     */
    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "Zero owner");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }

    /* ───────────── Minimal ERC20 ───────────── */

    function transfer(address to, uint256 amount) external returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        uint256 allowed = allowance[from][msg.sender];
        if (allowed != type(uint256).max) {
            allowance[from][msg.sender] = allowed - amount; // will revert if insufficient
        }
        _transfer(from, to, amount);
        return true;
    }

    function _transfer(address from, address to, uint256 amount) internal {
        require(to != address(0), "ERC20: transfer to zero");
        balanceOf[from] -= amount; // reverts if insufficient
        balanceOf[to]   += amount;
        emit Transfer(from, to, amount);
    }

    function _mint(address to, uint256 amount) internal {
        require(to != address(0), "ERC20: mint to zero");
        totalSupply += amount;
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) internal {
        balanceOf[from] -= amount; // reverts if insufficient
        totalSupply -= amount;
        emit Transfer(from, address(0), amount);
    }

    /* ───────────── RNG CORE ───────────── */

    /**
     * @notice Request RNG with open participation and app callback on finalize.
     * @dev Requires `msg.value >= oracleFee`. Excess is retained (deliberate).
     */
    function requestRng(uint64 waitBlocks, uint16 maxParticipants)
        external
        payable
        returns (uint256 id)
    {
        require(msg.value >= oracleFee, "Fee");
        id = _createRng(msg.sender, waitBlocks, maxParticipants, false, new address[](0), true);
    }

    /**
     * @notice Request RNG without callback to requester on finalize.
     */
    function requestRngNoCallback(uint64 waitBlocks, uint16 maxParticipants)
        external
        payable
        returns (uint256 id)
    {
        require(msg.value >= oracleFee, "Fee");
        id = _createRng(msg.sender, waitBlocks, maxParticipants, false, new address[](0), false);
    }

    /**
     * @notice Request RNG with a fixed whitelist (participants must exist in `wl`).
     */
    function requestRngWhitelisted(uint64 waitBlocks, address[] calldata wl)
        external
        payable
        returns (uint256 id)
    {
        require(msg.value >= oracleFee, "Fee");
        require(wl.length > 0 && wl.length <= 100, "wl size");
        id = _createRng(msg.sender, waitBlocks, uint16(wl.length), true, wl, true);
    }

    function _createRng(
        address requester,
        uint64 waitBlocks,
        uint16 maxParticipants,
        bool isWhitelisted,
        address[] memory wl,
        bool cb
    ) internal returns (uint256 id) {
        require(waitBlocks >= 10 && waitBlocks <= 200, "wait");
        require(maxParticipants > 0 && maxParticipants <= 100, "max");

        id = nextRngId++;
        RngRequest storage r = _rngRequests[id];
        r.requester = requester;
        r.deadline = uint64(block.number + waitBlocks);
        // expiryBlock is set to comfortably within the 256-block hash window
        r.expiryBlock = uint64(block.number + waitBlocks + 255);
        r.maxParticipants = maxParticipants;
        r.isWhitelisted = isWhitelisted;
        r.callbackOnFinalize = cb;

        if (isWhitelisted) {
            for (uint256 i = 0; i < wl.length; ) {
                address addr = wl[i];
                require(addr != address(0), "wl");
                r.whitelist[addr] = true;
                unchecked { ++i; }
            }
        }

        emit RngRequested(id, requester);
    }

    /**
     * @notice Join an RNG as a participant to be mixed into entropy and share rewards.
     */
    function joinRng(uint256 id) external {
        RngRequest storage r = _rngRequests[id];
        require(r.deadline != 0, "no rng");
        require(!r.finalized, "done");
        require(block.number < r.deadline, "deadline");
        require(!hasJoined[id][msg.sender], "joined");
        require(r.participantCount < r.maxParticipants, "full");
        if (r.isWhitelisted) require(r.whitelist[msg.sender], "not wl");

        hasJoined[id][msg.sender] = true;
        r.participants.push(msg.sender);
        r.participantCount++;
        emit RngJoined(id, msg.sender);
    }

    /**
     * @notice Finalize RNG: computes randomness using blockhash(deadline) optionally
     *         mixed with participants. Can succeed with ZERO participants (no rewards).
     * @dev    Off-by-one fixed: requires `block.number > deadline` (not >=).
     */
    function finalizeRng(uint256 id) public nonReentrant {
        RngRequest storage r = _rngRequests[id];
        require(!r.finalized, "finalized");
        require(block.number > r.deadline, "early");         // fixed: strictly greater than deadline
        require(block.number < r.expiryBlock, "expired");

        bytes32 e = blockhash(r.deadline);
        require(e != 0, "bh"); // ensure deadline is within the last 256 blocks

        // Mix participant addresses into entropy only if there are any
        uint256 pc = r.participantCount;
        if (pc > 0) {
            address[] storage parts = r.participants;
            for (uint256 i = 0; i < pc; ) {
                e = keccak256(abi.encodePacked(e, parts[i]));
                unchecked { ++i; }
            }

            // Reward: 1 token split among participants (exact-sum split)
            uint256 base = 1e18 / pc;
            uint256 total;
            for (uint256 i = 0; i + 1 < pc; ) {
                _mint(parts[i], base);
                total += base;
                unchecked { ++i; }
            }
            _mint(parts[pc - 1], 1e18 - total);
        }

        uint256 randomness = uint256(e);
        r.randomResult = randomness;
        r.finalized = true;

        emit RngFinalized(id, randomness);

        if (r.callbackOnFinalize && _isContract(r.requester)) {
            // Best-effort callback; ignore failures
            try IOracleRNGConsumer(r.requester).fulfillRandomness(id, randomness) {} catch {}
        }
    }

    /* ───────────── Views to aid ops/UX ───────────── */

    function getRngParticipants(uint256 id) external view returns (address[] memory) {
        return _rngRequests[id].participants;
    }

    function getRngMeta(uint256 id)
        external
        view
        returns (
            address requester,
            uint64 deadline,
            uint64 expiryBlock,
            uint16 maxParticipants,
            uint16 participantCount,
            bool isWhitelisted,
            bool callbackOnFinalize,
            bool finalized,
            uint256 randomResult
        )
    {
        RngRequest storage r = _rngRequests[id];
        return (
            r.requester,
            r.deadline,
            r.expiryBlock,
            r.maxParticipants,
            r.participantCount,
            r.isWhitelisted,
            r.callbackOnFinalize,
            r.finalized,
            r.randomResult
        );
    }

    /* ───────────── PvP (Player vs Player) ─────────────
       Operational guidance:
       - Pass `maxParticipants >= 2` if you rely on auto-joining both players.
       - Battles auto-join P1 & P2 to the RNG (best effort); if full or WL
         prevents joining, the zero-participant finalization patch prevents lockups.
       - Escape hatches are provided to unwind if RNG expires or battle never matches.
    ────────────────────────────────────────────────────*/

    function _joinRngInternal(uint256 id, address p) internal {
        RngRequest storage r = _rngRequests[id];
        if (r.finalized) return;
        if (block.number >= r.deadline) return; // ← prevent post‑deadline joins
        if (hasJoined[id][p]) return;
        if (r.participantCount >= r.maxParticipants) return;
        if (r.isWhitelisted && !r.whitelist[p]) return;

        hasJoined[id][p] = true;
        r.participants.push(p);
        r.participantCount++;
        emit RngJoined(id, p);
    }

    function createBattle(uint256 wager, uint64 waitBlocks, uint16 maxParticipants)
        external
        returns (uint256 bid)
    {
        require(wager > 0, "wager");
        require(balanceOf[msg.sender] >= wager, "bal");
        _burn(msg.sender, wager);

        // Create RNG owned by this contract; no callback needed for PvP
        uint256 rngId = _createRng(address(this), waitBlocks, maxParticipants, false, new address[](0), false);

        // Auto-join P1 (best effort; caps & WL are respected)
        _joinRngInternal(rngId, msg.sender);

        bid = nextBattleId++;
        Battle storage b = _battles[bid];
        b.player1 = msg.sender;
        b.wager = wager;
        b.rngId = rngId;
        b.state = BattleState.WaitingP2;

        _battleEscrow[bid] = wager;
        emit BattleCreated(bid, msg.sender, wager);
    }

    function matchBattle(uint256 bid) external {
        Battle storage b = _battles[bid];
        require(b.state == BattleState.WaitingP2, "state");
        require(b.player1 != msg.sender, "self");

        RngRequest storage r = _rngRequests[b.rngId];
        require(block.number < r.deadline, "rng closed"); // ← ensure RNG still open

        require(balanceOf[msg.sender] >= b.wager, "bal");
        _burn(msg.sender, b.wager);
        b.player2 = msg.sender;
        b.state = BattleState.WaitingRng;
        _battleEscrow[bid] += b.wager;
        emit BattleMatched(bid, msg.sender);

        _joinRngInternal(b.rngId, msg.sender);
    }

    /**
     * @notice Resolve a matched battle. Will try to finalize RNG on-demand.
     * @dev Uses an external call to `finalizeRng` to respect the nonReentrant guard.
     */
    function resolveBattle(uint256 bid) external {
        Battle storage b = _battles[bid];
        require(b.state == BattleState.WaitingRng, "state");

        RngRequest storage r = _rngRequests[b.rngId];

        // If possible, auto-finalize on demand to reduce UX friction.
        if (!r.finalized && block.number > r.deadline && block.number < r.expiryBlock) {
            // external call to observe nonReentrant guard within finalizeRng
            this.finalizeRng(b.rngId);
            r = _rngRequests[b.rngId]; // refresh storage ref
        }

        require(r.finalized, "rng");

        // Winner by LSB of randomness
        address winner = (r.randomResult & 1) == 0 ? b.player1 : b.player2;
        uint256 esc = _battleEscrow[bid];
        b.state = BattleState.Resolved;
        delete _battleEscrow[bid];

        _mint(winner, esc);
        emit BattleResolved(bid, winner, esc);
    }

    /**
     * @notice Cancel an open battle (no P2 yet). Returns escrow to P1.
     */
    function cancelOpenBattle(uint256 bid) external {
        Battle storage b = _battles[bid];
        require(b.state == BattleState.WaitingP2, "state");
        require(msg.sender == b.player1 || msg.sender == _owner, "auth");

        uint256 esc = _battleEscrow[bid];
        b.state = BattleState.Resolved;
        delete _battleEscrow[bid];

        _mint(b.player1, esc);
        emit BattleResolved(bid, b.player1, esc);
    }

    /**
     * @notice Cancel a stuck battle if RNG expired (missed blockhash window)
     *         OR no participants joined and deadline passed.
     * @dev    If P2 has joined, escrow is split back evenly; otherwise all to P1.
     */
    function cancelStuckBattle(uint256 bid) external {
        Battle storage b = _battles[bid];
        require(b.state == BattleState.WaitingRng, "state");
        require(msg.sender == b.player1 || msg.sender == b.player2 || msg.sender == _owner, "auth");

        RngRequest storage r = _rngRequests[b.rngId];
        bool rngExpired = block.number >= r.expiryBlock;
        bool noParticipantsAndPastDeadline = (block.number > r.deadline && r.participantCount == 0);
        require(rngExpired || noParticipantsAndPastDeadline, "not cancellable");

        uint256 esc = _battleEscrow[bid];
        b.state = BattleState.Resolved;
        delete _battleEscrow[bid];

        if (b.player2 == address(0)) {
            // Never matched
            _mint(b.player1, esc);
            emit BattleResolved(bid, b.player1, esc);
        } else {
            // Matched but stuck; refund fairly
            uint256 half = esc / 2;
            _mint(b.player1, half);
            _mint(b.player2, esc - half);
            emit BattleResolved(bid, address(0), esc); // zero-address signals cancel/refund
        }
    }

    /* ───────────── Admin Ops ───────────── */

    function setOracleFee(uint256 newFee) external onlyOwner {
        uint256 old = oracleFee;
        oracleFee = newFee;
        emit OracleFeeUpdated(old, newFee);
    }

    /**
     * @notice Withdraw accumulated request fees (ETH).
     * @dev    NonReentrant to avoid reentrancy via receiver hooks.
     */
    function withdrawFees(address payable to) external onlyOwner nonReentrant {
        uint256 amt = address(this).balance;
        (bool ok, ) = to.call{value: amt}("");
        require(ok, "transfer");
        emit FeeWithdrawn(to, amt);
    }

    /* ───────────── Helpers ───────────── */

    function _isContract(address a) internal view returns (bool) {
        return a.code.length > 0;
    }

    /* ───────────── Storage gap for future upgrades ───────────── */
    uint256[50] private __gap;
}

/*──────────────────────────────────────────────────────────────────────────────
│ DEPLOYMENT GUIDE
│
│ // 1) Deploy implementation
│ const Oracle = await ethers.getContractFactory("OracleRNG");
│ const impl = await Oracle.deploy();
│ await impl.deployed();
│
│ // 2) Encode initializer (use a MULTISIG as owner; must be non-zero!)
│ //    ethers v6:
│ const owner = "<YOUR_MULTISIG>";
│ const fee   = ethers.parseEther("0.001"); // or your chosen default
│ const initData = impl.interface.encodeFunctionData("initialize", [owner, fee]);
│
│ // 3) Deploy proxy; admin is UPGRADE AUTHORITY ONLY (ideally a multisig)
│ const Proxy = await ethers.getContractFactory("ERC1967Proxy");
│ const admin = "<YOUR_PROXY_ADMIN_MULTISIG>";
│ const proxy = await Proxy.deploy(impl.target, admin, initData); // ethers v6 uses .target
│ await proxy.deployed();
│
│ // 4) Interact with the proxied instance
│ const rng = Oracle.attach(proxy.target);
│
│ OPERATIONAL NOTES
│ - Proxy constructor now REQUIRES non-empty initializer calldata.
│ - OracleRNG.initialize REQUIRES non-zero owner (prevents proxy-bricking).
│ - Admin cannot call application functions via the proxy; use a separate EOA.
│ - For PvP, choose `maxParticipants >= 2` to ensure both players can auto-join.
│ - If the RNG deadline passes without participants, finalize still works (no rewards).
│ - If the RNG expires (missed blockhash window), use `cancelStuckBattle()`.
│ - This RNG is NOT cryptographically secure. Upgrade to VRF for high-stakes.
└──────────────────────────────────────────────────────────────────────────────*/

Tags:
Multisig, Upgradeable, Multi-Signature, Factory, Oracle|addr:0x41df754132756ed64bfe0eebf007dc1f90101caf|verified:true|block:23594598|tx:0x49373fa60e314480fe66c92682ecd7c8c2b23090bebf10c5e1b0441ee3a55bc6|first_check:1760690655

Submitted on: 2025-10-17 10:44:16

Comments

Log in to comment.

No comments yet.