♠️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