♠️Roulette Game Contract

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

// OpenZeppelin Address library
library Address {
    function isContract(address account) internal view returns (bool) {
        return account.code.length > 0;
    }

    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");
        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, "Address: low-level call failed");
    }

    function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            if (returndata.length > 0) {
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}

// NativeVRF interface (abridged for relevant functions)
interface INativeVRF {
    function requestRandom(uint256 numRequest) external payable returns (uint256[] memory);
    function randomResults(uint256 requestId) external view returns (uint256);
    function minReward() external view returns (uint256);
}

// Standard ReentrancyGuard implementation
contract ReentrancyGuard {
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    modifier nonReentrant() {
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
}

// Standard Ownable implementation
contract Ownable {
    address private _owner;
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        _transferOwnership(msg.sender);
    }

    modifier onlyOwner() {
        require(owner() == msg.sender, "Ownable: caller is not the owner");
        _;
    }

    function owner() public view returns (address) {
        return _owner;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

contract LazyBullRoulette is ReentrancyGuard, Ownable {
    using Address for address payable;

    INativeVRF public nativeVRF;

    enum BetType { StraightUp, RedBlack, Dozen }

    struct Bet {
        uint8 betType;
        uint8 number;
        uint256 amount;
    }

    struct SpinResult {
        address player;
        uint256 result;
        uint256 totalBetAmount;
        uint256 totalPayout;
        uint256 timestamp;
    }

    // Security constants
    uint256 public MAX_BET = 1 ether;
    uint256 public MIN_BANKROLL = 150 ether;
    uint256 public MAX_BANKROLL = 900 ether;
    uint8 public MAX_BETS_PER_SPIN = 5;
    uint256 public MAX_PAYOUT_PER_SPIN = 36 ether;
    uint256 public constant MIN_SPIN_INTERVAL = 3 seconds;

    bool public paused;

    mapping(uint256 => SpinResult) public spins;
    mapping(address => uint256) public winnings;
    mapping(uint256 => Bet[]) private spinBets;
    mapping(address => uint256) private pendingWinnings;
    uint256 private totalPendingWinnings;

    mapping(address => uint256) public lastSpinTime;
    mapping(address => uint256) public dailySpinCount;
    mapping(address => uint256) public lastDayReset;
    uint256 public constant MAX_DAILY_SPINS = 200;

    event SpinRequested(address indexed player, uint256 indexed requestId);
    event SpinRevealed(address indexed player, uint256 indexed requestId, uint256 result, uint256 payout);
    event WinningsWithdrawn(address indexed player, uint256 amount);
    event ParameterUpdated(string indexed parameter, uint256 oldValue, uint256 newValue);

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    modifier rateLimited() {
        require(block.timestamp >= lastSpinTime[msg.sender] + MIN_SPIN_INTERVAL, "Rate limit exceeded");
        uint256 dayStart = (block.timestamp / 86400) * 86400;
        if (lastDayReset[msg.sender] < dayStart) {
            dailySpinCount[msg.sender] = 0;
            lastDayReset[msg.sender] = dayStart;
        }
        lastSpinTime[msg.sender] = block.timestamp;
        dailySpinCount[msg.sender]++;
        require(dailySpinCount[msg.sender] <= MAX_DAILY_SPINS, "Daily spin limit exceeded");
        _;
    }

    constructor(address nativeVRFAddress) {
        nativeVRF = INativeVRF(nativeVRFAddress);
    }

    function requestSpin(Bet[] calldata bets) external payable nonReentrant whenNotPaused rateLimited {
        require(bets.length > 0, "No bets provided");
        require(bets.length <= MAX_BETS_PER_SPIN, "Too many bets");

        (uint256 totalBetAmount, uint256 maxPayout) = _validateBets(bets);
        uint256 minReward = nativeVRF.minReward();
        require(msg.value >= totalBetAmount + minReward, "Incorrect payment amount");
        require(address(this).balance >= MIN_BANKROLL + maxPayout + totalPendingWinnings, "Insufficient bankroll");
        require(maxPayout <= MAX_PAYOUT_PER_SPIN, "Exceeds maximum payout");

        uint256[] memory requestIds = nativeVRF.requestRandom{value: minReward}(1);
        uint256 requestId = requestIds[0];

        spins[requestId] = SpinResult({
            player: msg.sender,
            result: 0,
            totalBetAmount: totalBetAmount,
            totalPayout: 0,
            timestamp: block.timestamp
        });

        for (uint256 i = 0; i < bets.length; i++) {
            spinBets[requestId].push(bets[i]);
        }

        emit SpinRequested(msg.sender, requestId);

        uint256 excess = msg.value - totalBetAmount - minReward;
        if (excess > 0) payable(msg.sender).sendValue(excess);
    }

    function completeGame(uint256 requestId) external nonReentrant {
        SpinResult storage spin = spins[requestId];
        require(spin.player == msg.sender, "Not your game");
        require(spin.result == 0, "Game completed");

        uint256 random = nativeVRF.randomResults(requestId);
        require(random != 0, "Random not fulfilled yet");

        uint256 result = random % 38;
        uint256 totalPayout = 0;
        for (uint256 i = 0; i < spinBets[requestId].length; i++) {
            totalPayout += _getBetPayout(spinBets[requestId][i], result);
        }

        spin.result = result;
        spin.totalPayout = totalPayout;
        spin.timestamp = block.timestamp;

        if (totalPayout > 0) {
            winnings[spin.player] += totalPayout;
            pendingWinnings[spin.player] += totalPayout;
            totalPendingWinnings += totalPayout;
        }

        emit SpinRevealed(spin.player, requestId, result, totalPayout);
    }

    function refundUnfulfilledSpin(uint256 requestId) external nonReentrant {
        SpinResult storage spin = spins[requestId];
        require(spin.player == msg.sender, "Not owner");
        require(spin.result == 0, "Already processed");
        require(nativeVRF.randomResults(requestId) == 0, "Randomness exists");
        require(block.timestamp > spin.timestamp + 1 hours, "Refund window not open");

        uint256 refundAmt = spin.totalBetAmount;
        delete spins[requestId];
        delete spinBets[requestId];

        payable(msg.sender).sendValue(refundAmt);
        emit WinningsWithdrawn(msg.sender, refundAmt);
    }

    function _validateBets(Bet[] calldata bets) private view returns (uint256 totalBetAmount, uint256 maxPayout) {
        for (uint256 i = 0; i < bets.length; i++) {
            Bet calldata bet = bets[i];
            require(bet.amount > 0, "Zero bet");
            require(bet.amount <= MAX_BET, "Bet too high");

            if (bet.betType == uint8(BetType.StraightUp)) {
                require(bet.number <= 37, "Invalid number");
                maxPayout += bet.amount * 36;
            } else if (bet.betType == uint8(BetType.RedBlack)) {
                require(bet.number <= 1, "Invalid red/black");
                maxPayout += bet.amount * 2;
            } else if (bet.betType == uint8(BetType.Dozen)) {
                require(bet.number <= 2, "Invalid dozen");
                maxPayout += bet.amount * 3;
            } else {
                revert("Invalid bet type");
            }
            totalBetAmount += bet.amount;
        }
    }

    function _getBetPayout(Bet memory bet, uint256 result) private pure returns (uint256) {
        if (bet.betType == uint8(BetType.StraightUp)) {
            return bet.number == result ? bet.amount * 36 : 0;
        } else if (bet.betType == uint8(BetType.RedBlack)) {
            if (result == 0 || result == 37) return 0;
            bool isRed = (result == 1 || result == 3 || result == 5 || result == 7 || result == 9 || result == 12 || result == 14 || result == 16 || result == 18 || result == 19 || result == 21 || result == 23 || result == 25 || result == 27 || result == 30 || result == 32 || result == 34 || result == 36);
            return ((bet.number == 0 && isRed) || (bet.number == 1 && !isRed)) ? bet.amount * 2 : 0;
        } else if (bet.betType == uint8(BetType.Dozen)) {
            if (result == 0 || result == 37) return 0;
            uint256 dozen = (result - 1) / 12;
            return bet.number == dozen ? bet.amount * 3 : 0;
        }
        return 0;
    }

    function withdrawWinnings() external nonReentrant {
        uint256 amount = winnings[msg.sender];
        require(amount > 0, "No winnings");
        winnings[msg.sender] = 0;
        if (pendingWinnings[msg.sender] >= amount) {
            pendingWinnings[msg.sender] -= amount;
            totalPendingWinnings -= amount;
        }
        payable(msg.sender).sendValue(amount);
        emit WinningsWithdrawn(msg.sender, amount);
    }

    function deposit() external payable onlyOwner {
        require(msg.value > 0, "Must deposit");
        require(address(this).balance <= MAX_BANKROLL, "Exceeds max bankroll");
    }

    function withdrawOwner(uint256 amount) external onlyOwner {
        require(amount > 0, "Invalid");
        require(address(this).balance >= MIN_BANKROLL + totalPendingWinnings + amount, "Insufficient");
        payable(owner()).sendValue(amount);
    }

    function emergencyWithdraw() external onlyOwner {
        require(paused, "Not paused");
        uint256 bal = address(this).balance;
        require(bal > totalPendingWinnings, "Preserve winnings");
        payable(owner()).sendValue(bal - totalPendingWinnings);
    }

    function pause() external onlyOwner { paused = true; }
    function unpause() external onlyOwner { paused = false; }

    function setMaxBet(uint256 newMax) external onlyOwner {
        require(newMax > 0 && newMax <= 10 ether, "Invalid");
        emit ParameterUpdated("MAX_BET", MAX_BET, newMax);
        MAX_BET = newMax;
    }

    function setMinBankroll(uint256 newMin) external onlyOwner {
        require(newMin > 0 && newMin <= MAX_BANKROLL, "Invalid");
        emit ParameterUpdated("MIN_BANKROLL", MIN_BANKROLL, newMin);
        MIN_BANKROLL = newMin;
    }

    function setMaxBankroll(uint256 newMax) external onlyOwner {
        require(newMax >= MIN_BANKROLL && newMax <= 2000 ether, "Invalid");
        emit ParameterUpdated("MAX_BANKROLL", MAX_BANKROLL, newMax);
        MAX_BANKROLL = newMax;
    }

    function setMaxBetsPerSpin(uint8 newMax) external onlyOwner {
        require(newMax > 0 && newMax <= 20, "Invalid");
        emit ParameterUpdated("MAX_BETS_PER_SPIN", MAX_BETS_PER_SPIN, newMax);
        MAX_BETS_PER_SPIN = newMax;
    }

    function setMaxPayoutPerSpin(uint256 newMax) external onlyOwner {
        require(newMax > 0 && newMax <= MIN_BANKROLL / 2, "Invalid");
        emit ParameterUpdated("MAX_PAYOUT_PER_SPIN", MAX_PAYOUT_PER_SPIN, newMax);
        MAX_PAYOUT_PER_SPIN = newMax;
    }

    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }

    function getUserStats(address user) external view returns (uint256 userWinnings, uint256 lastSpin, uint256 spinsToday, bool canSpin) {
        userWinnings = winnings[user];
        lastSpin = lastSpinTime[user];
        spinsToday = dailySpinCount[user];
        canSpin = block.timestamp >= lastSpin + MIN_SPIN_INTERVAL && spinsToday < MAX_DAILY_SPINS && !paused;
    }

    function getSpinBets(uint256 requestId) external view returns (Bet[] memory) {
        return spinBets[requestId];
    }

    function getPendingWinnings(address user) external view returns (uint256) {
        return pendingWinnings[user];
    }

    function getMinReward() external view returns (uint256) {
        return nativeVRF.minReward();
    }

    function isRandomnessAvailable(uint256 requestId) external view returns (bool) {
        return nativeVRF.randomResults(requestId) != 0;
    }
}

Last updated