Skip to content

Instantly share code, notes, and snippets.

@Dhaiwat10
Created October 1, 2025 14:35
Show Gist options
  • Save Dhaiwat10/3729299890f917cb2daca5833dd79e8b to your computer and use it in GitHub Desktop.
Save Dhaiwat10/3729299890f917cb2daca5833dd79e8b to your computer and use it in GitHub Desktop.
// 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