Created
October 1, 2025 14:35
-
-
Save Dhaiwat10/3729299890f917cb2daca5833dd79e8b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.20; | |
| interface IVRF { | |
| function requestRandomness() external returns (uint256 requestId); | |
| } | |
| contract SlotMachine5 { | |
| IVRF public vrf; | |
| address public owner; | |
| address public constant owner2 = 0x80874B9Dbe983adF0dB56Cc71F0167D8385c7A61; | |
| uint256 public constant FIXED_BET = 0.1 ether; | |
| uint256 public feeBps = 100; // 1% | |
| // Sum of maximum possible payouts (50x bet) for all pending spins | |
| uint256 public pendingExposure; | |
| mapping(uint256 => address) public requestToPlayer; | |
| mapping(uint256 => uint256) public requestToBet; // net bet | |
| mapping(uint256 => uint256) public requestTimestamp; | |
| // Track all active request IDs | |
| uint256[] public activeRequestIds; | |
| mapping(uint256 => bool) public isRequestActive; | |
| uint256 public accumulatedFees; | |
| uint256 public constant CANCEL_DELAY = 1 minutes; | |
| uint256 public constant PLAYER_CANCEL_DELAY = 5 minutes; | |
| // Wager statistics | |
| uint256 public totalSpins; | |
| uint256 public totalWagered; | |
| uint256 public totalPaidOut; | |
| event SpinRequested( | |
| address indexed player, | |
| uint256 grossBet, | |
| uint256 netBet, | |
| uint256 requestId | |
| ); | |
| event SpinResult( | |
| address indexed player, | |
| uint256 bet, | |
| uint256[5] reels, | |
| uint256 payout, | |
| uint256 randomnessRound | |
| ); | |
| event SpinCancelled(address indexed player, uint256 netBet, uint256 requestId); | |
| modifier onlyOwners() { | |
| require(msg.sender == owner || msg.sender == owner2, "Not owner"); | |
| _; | |
| } | |
| constructor(address _vrf) { | |
| vrf = IVRF(_vrf); | |
| owner = msg.sender; | |
| } | |
| // deposit bankroll | |
| function fund() external payable {} | |
| // withdraw bankroll (cannot withdraw funds reserved for pending exposure) | |
| function withdraw(uint256 amount) external onlyOwners { | |
| //Disable for now | |
| require( | |
| amount <= address(this).balance - pendingExposure, | |
| "Insufficient free balance" | |
| ); | |
| payable(owner).transfer(amount); | |
| } | |
| function spin() external payable { | |
| require(msg.value == FIXED_BET, "Bet must be exactly 0.01 ETH"); | |
| uint256 fee = (FIXED_BET * feeBps) / 10_000; | |
| uint256 netBet = FIXED_BET - fee; | |
| // Reserve exposure for this pending spin using net bet (50x max payout) | |
| uint256 newExposure = pendingExposure + (netBet * 50); | |
| require(address(this).balance >= newExposure, "Bankroll too low"); | |
| uint256 requestId = vrf.requestRandomness(); | |
| require(requestToPlayer[requestId] == address(0), "Duplicate request"); | |
| requestToPlayer[requestId] = msg.sender; | |
| requestToBet[requestId] = netBet; | |
| requestTimestamp[requestId] = block.timestamp; | |
| pendingExposure = newExposure; | |
| // Track this as an active request | |
| activeRequestIds.push(requestId); | |
| isRequestActive[requestId] = true; | |
| // Update wager statistics | |
| totalSpins++; | |
| totalWagered += FIXED_BET; | |
| // Accumulate fee to be withdrawn later | |
| accumulatedFees += fee; | |
| emit SpinRequested(msg.sender, msg.value, netBet, requestId); | |
| } | |
| // VRF callback | |
| function fulfillRandomness(uint256 requestId, uint256 rand, uint256 randomnessRound) external { | |
| require(msg.sender == address(vrf), "Only VRF"); | |
| address player = requestToPlayer[requestId]; | |
| uint256 bet = requestToBet[requestId]; | |
| require(player != address(0), "No such request"); | |
| delete requestToPlayer[requestId]; | |
| delete requestToBet[requestId]; | |
| // Mark request as inactive | |
| isRequestActive[requestId] = false; | |
| // Release the reserved exposure for this request | |
| pendingExposure -= (bet * 50); | |
| // derive 5 reels using hashed expansions to avoid digit bias | |
| uint256[5] memory reels; | |
| reels[0] = uint256(keccak256(abi.encodePacked(rand, uint256(0)))) % 10; | |
| reels[1] = uint256(keccak256(abi.encodePacked(rand, uint256(1)))) % 10; | |
| reels[2] = uint256(keccak256(abi.encodePacked(rand, uint256(2)))) % 10; | |
| reels[3] = uint256(keccak256(abi.encodePacked(rand, uint256(3)))) % 10; | |
| reels[4] = uint256(keccak256(abi.encodePacked(rand, uint256(4)))) % 10; | |
| uint256 payout = _calculatePayout(reels, bet); | |
| if (payout > 0) { | |
| totalPaidOut += payout; | |
| payable(player).transfer(payout); | |
| } | |
| emit SpinResult(player, bet, reels, payout, randomnessRound); | |
| } | |
| function setFeeBps(uint256 newFeeBps) external onlyOwners { | |
| require(newFeeBps <= 500, "Fee too high"); | |
| feeBps = newFeeBps; | |
| } | |
| function cancelRequest(uint256 requestId) external { | |
| address player = requestToPlayer[requestId]; | |
| uint256 bet = requestToBet[requestId]; | |
| require(player != address(0), "No such request"); | |
| uint256 createdAt = requestTimestamp[requestId]; | |
| if (msg.sender == owner || msg.sender == owner2) { | |
| require(block.timestamp >= createdAt + CANCEL_DELAY, "Too early"); | |
| } else if (msg.sender == player) { | |
| require(block.timestamp >= createdAt + PLAYER_CANCEL_DELAY, "Too early"); | |
| } else { | |
| revert("Not authorized"); | |
| } | |
| delete requestToPlayer[requestId]; | |
| delete requestToBet[requestId]; | |
| delete requestTimestamp[requestId]; | |
| // Mark request as inactive | |
| isRequestActive[requestId] = false; | |
| // Release exposure and refund net bet | |
| pendingExposure -= (bet * 50); | |
| payable(player).transfer(bet); | |
| emit SpinCancelled(player, bet, requestId); | |
| } | |
| function withdrawFees(uint256 amount) external onlyOwners { | |
| require(amount <= accumulatedFees, "Amount exceeds fees"); | |
| require(amount <= address(this).balance - pendingExposure, "Insufficient free balance"); // deploy for now | |
| accumulatedFees -= amount; | |
| payable(owner).transfer(amount); | |
| } | |
| function getRTPBps() external view returns (uint256 preFeeRtpBps, uint256 effectiveRtpBps) { | |
| // Based on 5-reel structure with 50x max payout | |
| // 50x: 1 combo = 0.05% RTP | |
| // 25x: 9 combos = 0.225% RTP | |
| // 10x: 450 combos (45 four-7s + 405 four-of-kind) = 4.5% RTP | |
| // 5x: 900 combos (full house) = 4.5% RTP | |
| // 3x: 13,500 combos (1,350 three-7s + 12,150 three-of-kind) = 40.5% RTP | |
| // 2x: 8,100 combos (two pairs) = 16.2% RTP | |
| // 1x: 31,000 combos (27,000 one pair + 4,000 sequential) = 31% RTP | |
| // Total theoretical RTP: 96.975% | |
| preFeeRtpBps = 9_697; | |
| effectiveRtpBps = (preFeeRtpBps * (10_000 - feeBps)) / 10_000; | |
| } | |
| // Get all pending requests that can be cancelled | |
| function getPendingRequests() external view returns ( | |
| uint256[] memory requestIds, | |
| address[] memory players, | |
| uint256[] memory bets, | |
| uint256[] memory timestamps | |
| ) { | |
| uint256 count = 0; | |
| // Count active requests | |
| for (uint256 i = 0; i < activeRequestIds.length; i++) { | |
| if (isRequestActive[activeRequestIds[i]]) { | |
| count++; | |
| } | |
| } | |
| // Populate arrays | |
| requestIds = new uint256[](count); | |
| players = new address[](count); | |
| bets = new uint256[](count); | |
| timestamps = new uint256[](count); | |
| uint256 index = 0; | |
| for (uint256 i = 0; i < activeRequestIds.length; i++) { | |
| uint256 requestId = activeRequestIds[i]; | |
| if (isRequestActive[requestId]) { | |
| requestIds[index] = requestId; | |
| players[index] = requestToPlayer[requestId]; | |
| bets[index] = requestToBet[requestId]; | |
| timestamps[index] = requestTimestamp[requestId]; | |
| index++; | |
| } | |
| } | |
| return (requestIds, players, bets, timestamps); | |
| } | |
| // Clean up inactive requests from the array (gas optimization) | |
| function cleanupInactiveRequests() external { | |
| uint256[] memory newActiveRequests = new uint256[](activeRequestIds.length); | |
| uint256 count = 0; | |
| for (uint256 i = 0; i < activeRequestIds.length; i++) { | |
| if (isRequestActive[activeRequestIds[i]]) { | |
| newActiveRequests[count] = activeRequestIds[i]; | |
| count++; | |
| } | |
| } | |
| // Resize and update | |
| activeRequestIds = new uint256[](count); | |
| for (uint256 i = 0; i < count; i++) { | |
| activeRequestIds[i] = newActiveRequests[i]; | |
| } | |
| } | |
| // Get cancellable requests (past timeout) | |
| function getCancellableRequests() external view returns (uint256[] memory) { | |
| uint256 count = 0; | |
| // Count cancellable | |
| for (uint256 i = 0; i < activeRequestIds.length; i++) { | |
| uint256 requestId = activeRequestIds[i]; | |
| if (isRequestActive[requestId]) { | |
| uint256 timestamp = requestTimestamp[requestId]; | |
| if (block.timestamp >= timestamp + CANCEL_DELAY) { | |
| count++; | |
| } | |
| } | |
| } | |
| // Populate array | |
| uint256[] memory cancellable = new uint256[](count); | |
| uint256 index = 0; | |
| for (uint256 i = 0; i < activeRequestIds.length; i++) { | |
| uint256 requestId = activeRequestIds[i]; | |
| if (isRequestActive[requestId]) { | |
| uint256 timestamp = requestTimestamp[requestId]; | |
| if (block.timestamp >= timestamp + CANCEL_DELAY) { | |
| cancellable[index] = requestId; | |
| index++; | |
| } | |
| } | |
| } | |
| return cancellable; | |
| } | |
| function _calculatePayout( | |
| uint256[5] memory reels, | |
| uint256 bet | |
| ) internal pure returns (uint256) { | |
| // 50x: Five 7s | |
| if (reels[0] == 7 && reels[1] == 7 && reels[2] == 7 && | |
| reels[3] == 7 && reels[4] == 7) { | |
| return bet * 50; | |
| } | |
| // 25x: Five of a kind (not 7s) | |
| if (_isFiveOfAKind(reels) && reels[0] != 7) { | |
| return bet * 25; | |
| } | |
| // 10x: Four 7s or Four of a kind | |
| if (_hasFourSevens(reels) || _isFourOfAKind(reels)) { | |
| return bet * 10; | |
| } | |
| // 5x: Full house (3 of a kind + pair) | |
| if (_isFullHouse(reels)) { | |
| return bet * 5; | |
| } | |
| // 3x: Three 7s or Three of a kind | |
| if (_hasThreeSevens(reels) || _isThreeOfAKind(reels)) { | |
| return bet * 3; | |
| } | |
| // 2x: Two pairs | |
| if (_isTwoPairs(reels)) { | |
| return bet * 2; | |
| } | |
| // 1x: One pair or Sequential run | |
| if (_hasOnePair(reels) || _isSequential(reels)) { | |
| return bet * 1; | |
| } | |
| return 0; | |
| } | |
| function _isFiveOfAKind(uint256[5] memory reels) internal pure returns (bool) { | |
| return reels[0] == reels[1] && reels[1] == reels[2] && | |
| reels[2] == reels[3] && reels[3] == reels[4]; | |
| } | |
| function _isFourOfAKind(uint256[5] memory reels) internal pure returns (bool) { | |
| uint256[10] memory counts; | |
| for (uint i = 0; i < 5; i++) { | |
| counts[reels[i]]++; | |
| } | |
| for (uint i = 0; i < 10; i++) { | |
| if (counts[i] == 4) return true; | |
| } | |
| return false; | |
| } | |
| function _isFullHouse(uint256[5] memory reels) internal pure returns (bool) { | |
| uint256[10] memory counts; | |
| for (uint i = 0; i < 5; i++) { | |
| counts[reels[i]]++; | |
| } | |
| bool hasThree = false; | |
| bool hasTwo = false; | |
| for (uint i = 0; i < 10; i++) { | |
| if (counts[i] == 3) hasThree = true; | |
| if (counts[i] == 2) hasTwo = true; | |
| } | |
| return hasThree && hasTwo; | |
| } | |
| function _isThreeOfAKind(uint256[5] memory reels) internal pure returns (bool) { | |
| uint256[10] memory counts; | |
| for (uint i = 0; i < 5; i++) { | |
| counts[reels[i]]++; | |
| } | |
| for (uint i = 0; i < 10; i++) { | |
| if (counts[i] == 3) return true; | |
| } | |
| return false; | |
| } | |
| function _isTwoPairs(uint256[5] memory reels) internal pure returns (bool) { | |
| uint256[10] memory counts; | |
| for (uint i = 0; i < 5; i++) { | |
| counts[reels[i]]++; | |
| } | |
| uint256 pairs = 0; | |
| for (uint i = 0; i < 10; i++) { | |
| if (counts[i] == 2) pairs++; | |
| } | |
| return pairs == 2; | |
| } | |
| function _isSequential(uint256[5] memory reels) internal pure returns (bool) { | |
| // Sort the reels first | |
| uint256[5] memory sorted = reels; | |
| for (uint i = 0; i < 4; i++) { | |
| for (uint j = i + 1; j < 5; j++) { | |
| if (sorted[i] > sorted[j]) { | |
| uint256 temp = sorted[i]; | |
| sorted[i] = sorted[j]; | |
| sorted[j] = temp; | |
| } | |
| } | |
| } | |
| // Check if consecutive | |
| for (uint i = 0; i < 4; i++) { | |
| if (sorted[i + 1] != sorted[i] + 1) return false; | |
| } | |
| return true; | |
| } | |
| function _hasFourSevens(uint256[5] memory reels) internal pure returns (bool) { | |
| uint256 count = 0; | |
| for (uint i = 0; i < 5; i++) { | |
| if (reels[i] == 7) count++; | |
| } | |
| return count == 4; | |
| } | |
| function _hasThreeSevens(uint256[5] memory reels) internal pure returns (bool) { | |
| uint256 count = 0; | |
| for (uint i = 0; i < 5; i++) { | |
| if (reels[i] == 7) count++; | |
| } | |
| return count == 3; | |
| } | |
| function _hasOnePair(uint256[5] memory reels) internal pure returns (bool) { | |
| uint256[10] memory counts; | |
| for (uint i = 0; i < 5; i++) { | |
| counts[reels[i]]++; | |
| } | |
| uint256 pairs = 0; | |
| for (uint i = 0; i < 10; i++) { | |
| if (counts[i] == 2) pairs++; | |
| } | |
| return pairs == 1; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment