Skip to content

Commit 6c5efbc

Browse files
committed
feat: add dice, crash, coinflip, blackjack, jackpot, plinko, slots game solidity smart contract
1 parent e8f9164 commit 6c5efbc

File tree

7 files changed

+704
-0
lines changed

7 files changed

+704
-0
lines changed

web3/evm/src/Blackjack.sol

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import "@openzeppelin/contracts/access/Ownable.sol";
7+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
8+
import "./interfaces/ICasinoGame.sol";
9+
import "./libraries/CasinoMath.sol";
10+
11+
contract Blackjack is ICasinoGame, Ownable, ReentrancyGuard {
12+
using SafeERC20 for IERC20;
13+
14+
GameConfig public config;
15+
mapping(uint256 => BlackjackGame) public games;
16+
uint256 public gameCounter;
17+
IERC20 public token;
18+
19+
struct BlackjackGame {
20+
address player;
21+
uint256 betAmount;
22+
uint8[] playerCards;
23+
uint8[] dealerCards;
24+
uint8 playerScore;
25+
uint8 dealerScore;
26+
uint8 gameState; // 0=betting, 1=playing, 2=settled
27+
uint256 timestamp;
28+
}
29+
30+
event GameStarted(address indexed player, uint256 indexed gameId, uint256 betAmount);
31+
event CardDealt(address indexed player, uint256 indexed gameId, bool isPlayer, uint8 card);
32+
event GameSettled(address indexed player, uint256 indexed gameId, uint256 payout);
33+
34+
constructor(address _owner, address _token) Ownable(_owner) {
35+
token = IERC20(_token);
36+
}
37+
38+
function initialize(
39+
address treasury,
40+
uint256 minBet,
41+
uint256 maxBet,
42+
uint16 houseEdgeBps
43+
) external onlyOwner {
44+
config = GameConfig({
45+
treasury: treasury,
46+
minBet: minBet,
47+
maxBet: maxBet,
48+
houseEdgeBps: houseEdgeBps,
49+
paused: false
50+
});
51+
}
52+
53+
function placeBet(uint256 betAmount) external nonReentrant {
54+
require(!config.paused, "Game paused");
55+
CasinoMath.validateBet(betAmount, config.minBet, config.maxBet);
56+
57+
token.safeTransferFrom(msg.sender, config.treasury, betAmount);
58+
59+
uint256 seed = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender)));
60+
uint8 playerCard1 = uint8(CasinoMath.generateRandom(seed, 12) + 1);
61+
uint8 playerCard2 = uint8(CasinoMath.generateRandom(seed + 1, 12) + 1);
62+
uint8 dealerCard1 = uint8(CasinoMath.generateRandom(seed + 2, 12) + 1);
63+
64+
uint256 gameId = ++gameCounter;
65+
BlackjackGame storage game = games[gameId];
66+
game.player = msg.sender;
67+
game.betAmount = betAmount;
68+
game.playerCards.push(playerCard1);
69+
game.playerCards.push(playerCard2);
70+
game.dealerCards.push(dealerCard1);
71+
game.playerScore = calculateScore(game.playerCards);
72+
game.dealerScore = calculateScore(game.dealerCards);
73+
game.gameState = 1;
74+
game.timestamp = block.timestamp;
75+
76+
// Check for blackjack
77+
if (game.playerScore == 21) {
78+
game.gameState = 2;
79+
uint256 payout = (betAmount * 3) / 2; // 3:2 payout
80+
token.safeTransferFrom(config.treasury, msg.sender, payout);
81+
emit GameSettled(msg.sender, gameId, payout);
82+
}
83+
84+
emit GameStarted(msg.sender, gameId, betAmount);
85+
}
86+
87+
function hit(uint256 gameId) external nonReentrant {
88+
BlackjackGame storage game = games[gameId];
89+
require(game.gameState == 1, "Invalid state");
90+
require(game.player == msg.sender, "Not your game");
91+
92+
uint256 seed = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, gameId)));
93+
uint8 newCard = uint8(CasinoMath.generateRandom(seed, 12) + 1);
94+
game.playerCards.push(newCard);
95+
game.playerScore = calculateScore(game.playerCards);
96+
97+
if (game.playerScore > 21) {
98+
game.gameState = 2;
99+
emit GameSettled(msg.sender, gameId, 0);
100+
}
101+
}
102+
103+
function stand(uint256 gameId) external nonReentrant {
104+
BlackjackGame storage game = games[gameId];
105+
require(game.gameState == 1, "Invalid state");
106+
require(game.player == msg.sender, "Not your game");
107+
108+
// Dealer draws until 17+
109+
uint256 seed = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, gameId)));
110+
while (game.dealerScore < 17) {
111+
uint8 newCard = uint8(CasinoMath.generateRandom(seed + game.dealerCards.length, 12) + 1);
112+
game.dealerCards.push(newCard);
113+
game.dealerScore = calculateScore(game.dealerCards);
114+
}
115+
116+
game.gameState = 2;
117+
118+
uint256 payout = 0;
119+
if (game.dealerScore > 21 || game.playerScore > game.dealerScore) {
120+
payout = game.betAmount * 2; // 1:1 payout
121+
} else if (game.playerScore == game.dealerScore) {
122+
payout = game.betAmount; // Push
123+
}
124+
125+
if (payout > 0) {
126+
token.safeTransferFrom(config.treasury, msg.sender, payout);
127+
}
128+
129+
emit GameSettled(msg.sender, gameId, payout);
130+
}
131+
132+
function calculateScore(uint8[] memory cards) internal pure returns (uint8) {
133+
uint8 score = 0;
134+
uint8 aces = 0;
135+
136+
for (uint i = 0; i < cards.length; i++) {
137+
uint8 value = cards[i] % 13;
138+
if (value == 0) {
139+
aces++;
140+
score += 11;
141+
} else if (value >= 10) {
142+
score += 10;
143+
} else {
144+
score += value + 1;
145+
}
146+
}
147+
148+
while (score > 21 && aces > 0) {
149+
score -= 10;
150+
aces--;
151+
}
152+
153+
return score;
154+
}
155+
156+
function pause() external onlyOwner {
157+
config.paused = true;
158+
}
159+
160+
function unpause() external onlyOwner {
161+
config.paused = false;
162+
}
163+
}
164+

web3/evm/src/CoinFlip.sol

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import "@openzeppelin/contracts/access/Ownable.sol";
7+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
8+
import "./interfaces/ICasinoGame.sol";
9+
import "./libraries/CasinoMath.sol";
10+
11+
contract CoinFlip is ICasinoGame, Ownable, ReentrancyGuard {
12+
using SafeERC20 for IERC20;
13+
14+
GameConfig public config;
15+
mapping(uint256 => GameState) public games;
16+
uint256 public gameCounter;
17+
IERC20 public token;
18+
19+
event GamePlayed(
20+
address indexed player,
21+
uint256 indexed gameId,
22+
uint256 betAmount,
23+
uint256 choice,
24+
uint256 result,
25+
uint256 payout
26+
);
27+
28+
constructor(address _owner, address _token) Ownable(_owner) {
29+
token = IERC20(_token);
30+
}
31+
32+
function initialize(
33+
address treasury,
34+
uint256 minBet,
35+
uint256 maxBet,
36+
uint16 houseEdgeBps
37+
) external onlyOwner {
38+
config = GameConfig({
39+
treasury: treasury,
40+
minBet: minBet,
41+
maxBet: maxBet,
42+
houseEdgeBps: houseEdgeBps,
43+
paused: false
44+
});
45+
}
46+
47+
function play(uint256 betAmount, uint8 choice) external nonReentrant {
48+
require(!config.paused, "Game paused");
49+
require(choice <= 1, "Invalid choice");
50+
CasinoMath.validateBet(betAmount, config.minBet, config.maxBet);
51+
52+
token.safeTransferFrom(msg.sender, config.treasury, betAmount);
53+
54+
uint256 seed = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender)));
55+
uint256 result = CasinoMath.generateRandom(seed, 1);
56+
57+
uint256 gameId = ++gameCounter;
58+
bool won = (choice == result);
59+
uint256 payout = 0;
60+
61+
if (won) {
62+
// 1.95x payout = 19500 bps
63+
payout = CasinoMath.calculatePayout(betAmount, 19500, config.houseEdgeBps);
64+
token.safeTransferFrom(config.treasury, msg.sender, payout);
65+
}
66+
67+
games[gameId] = GameState({
68+
player: msg.sender,
69+
betAmount: betAmount,
70+
gameId: gameId,
71+
timestamp: block.timestamp,
72+
settled: true,
73+
result: result,
74+
payout: payout
75+
});
76+
77+
emit GamePlayed(msg.sender, gameId, betAmount, choice, result, payout);
78+
}
79+
80+
function pause() external onlyOwner {
81+
config.paused = true;
82+
}
83+
84+
function unpause() external onlyOwner {
85+
config.paused = false;
86+
}
87+
}
88+

web3/evm/src/Crash.sol

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import "@openzeppelin/contracts/access/Ownable.sol";
7+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
8+
import "./interfaces/ICasinoGame.sol";
9+
import "./libraries/CasinoMath.sol";
10+
11+
contract Crash is ICasinoGame, Ownable, ReentrancyGuard {
12+
using SafeERC20 for IERC20;
13+
14+
GameConfig public config;
15+
mapping(uint256 => GameState) public games;
16+
uint256 public gameCounter;
17+
18+
event BetPlaced(address indexed player, uint256 indexed gameId, uint256 betAmount, uint256 autoCashout);
19+
event Cashout(address indexed player, uint256 indexed gameId, uint256 multiplier, uint256 payout);
20+
event Crashed(uint256 indexed gameId, uint256 multiplier);
21+
22+
constructor(address _owner) Ownable(_owner) {}
23+
24+
function initialize(
25+
address treasury,
26+
uint256 minBet,
27+
uint256 maxBet,
28+
uint16 houseEdgeBps
29+
) external onlyOwner {
30+
config = GameConfig({
31+
treasury: treasury,
32+
minBet: minBet,
33+
maxBet: maxBet,
34+
houseEdgeBps: houseEdgeBps,
35+
paused: false
36+
});
37+
}
38+
39+
function placeBet(uint256 betAmount, uint256 autoCashout) external nonReentrant {
40+
require(!config.paused, "Game paused");
41+
CasinoMath.validateBet(betAmount, config.minBet, config.maxBet);
42+
43+
IERC20 token = IERC20(msg.sender); // Assume token passed via context
44+
token.safeTransferFrom(msg.sender, config.treasury, betAmount);
45+
46+
uint256 gameId = ++gameCounter;
47+
games[gameId] = GameState({
48+
player: msg.sender,
49+
betAmount: betAmount,
50+
gameId: gameId,
51+
timestamp: block.timestamp,
52+
settled: false,
53+
result: autoCashout,
54+
payout: 0
55+
});
56+
57+
emit BetPlaced(msg.sender, gameId, betAmount, autoCashout);
58+
}
59+
60+
function cashout(uint256 gameId, uint256 currentMultiplier) external nonReentrant {
61+
GameState storage game = games[gameId];
62+
require(!game.settled, "Game already settled");
63+
require(game.player == msg.sender, "Not your game");
64+
65+
uint256 multiplierBps = currentMultiplier * 100;
66+
uint256 payout = CasinoMath.calculatePayout(game.betAmount, multiplierBps, config.houseEdgeBps);
67+
68+
game.settled = true;
69+
game.result = currentMultiplier;
70+
game.payout = payout;
71+
72+
IERC20 token = IERC20(msg.sender);
73+
token.safeTransferFrom(config.treasury, msg.sender, payout);
74+
75+
emit Cashout(msg.sender, gameId, currentMultiplier, payout);
76+
}
77+
78+
function settleCrashed(uint256 gameId) external onlyOwner {
79+
GameState storage game = games[gameId];
80+
require(!game.settled, "Game already settled");
81+
82+
game.settled = true;
83+
game.payout = 0;
84+
85+
emit Crashed(gameId, game.result);
86+
}
87+
88+
function pause() external onlyOwner {
89+
config.paused = true;
90+
}
91+
92+
function unpause() external onlyOwner {
93+
config.paused = false;
94+
}
95+
}
96+

0 commit comments

Comments
 (0)