Skip to content

Commit e8f9164

Browse files
committed
feat: add solana lottery smart contract
1 parent a1752da commit e8f9164

File tree

6 files changed

+379
-0
lines changed

6 files changed

+379
-0
lines changed

web3/evm/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
out/
2+
cache/
3+
broadcast/
4+
*.sol
5+
!src/**/*.sol
6+
!script/**/*.sol
7+
!test/**/*.sol
8+
.env
9+
.env.local
10+

web3/evm/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "multi-chain-casino-evm",
3+
"version": "1.0.0",
4+
"description": "EVM smart contracts for Multi-Chain Casino Games",
5+
"scripts": {
6+
"build": "forge build",
7+
"test": "forge test",
8+
"test:verbose": "forge test -vvv",
9+
"test:gas": "forge test --gas-report",
10+
"deploy:sepolia": "forge script script/Deploy.s.sol --rpc-url sepolia --broadcast --verify",
11+
"deploy:mainnet": "forge script script/Deploy.s.sol --rpc-url mainnet --broadcast --verify",
12+
"lint": "forge fmt --check",
13+
"format": "forge fmt"
14+
},
15+
"dependencies": {
16+
"@chainlink/contracts": "^0.8.0",
17+
"@openzeppelin/contracts": "^5.0.0"
18+
}
19+
}
20+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
interface ICasinoGame {
5+
struct GameConfig {
6+
address treasury;
7+
uint256 minBet;
8+
uint256 maxBet;
9+
uint16 houseEdgeBps; // Basis points (e.g., 250 = 2.5%)
10+
bool paused;
11+
}
12+
13+
struct GameState {
14+
address player;
15+
uint256 betAmount;
16+
uint256 gameId;
17+
uint256 timestamp;
18+
bool settled;
19+
uint256 result;
20+
uint256 payout;
21+
}
22+
23+
event GamePlayed(
24+
address indexed player,
25+
uint256 indexed gameId,
26+
uint256 betAmount,
27+
uint256 result,
28+
uint256 payout
29+
);
30+
31+
function initialize(
32+
address treasury,
33+
uint256 minBet,
34+
uint256 maxBet,
35+
uint16 houseEdgeBps
36+
) external;
37+
38+
function pause() external;
39+
function unpause() external;
40+
}
41+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
library CasinoMath {
5+
error InvalidBetAmount();
6+
error BetTooLow();
7+
error BetTooHigh();
8+
error MathOverflow();
9+
10+
function calculatePayout(
11+
uint256 betAmount,
12+
uint256 multiplierBps,
13+
uint16 houseEdgeBps
14+
) internal pure returns (uint256) {
15+
uint256 payout = (betAmount * multiplierBps) / 10000;
16+
uint256 houseEdge = (payout * houseEdgeBps) / 10000;
17+
return payout - houseEdge;
18+
}
19+
20+
function validateBet(
21+
uint256 betAmount,
22+
uint256 minBet,
23+
uint256 maxBet
24+
) internal pure {
25+
if (betAmount < minBet) revert BetTooLow();
26+
if (betAmount > maxBet) revert BetTooHigh();
27+
}
28+
29+
function generateRandom(uint256 seed, uint256 max) internal view returns (uint256) {
30+
return uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, seed))) % (max + 1);
31+
}
32+
}
33+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "lottery"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib", "lib"]
8+
name = "lottery"
9+
10+
[features]
11+
no-entrypoint = []
12+
no-idl = []
13+
no-log-ix-name = []
14+
cpi = ["no-entrypoint"]
15+
default = []
16+
17+
[dependencies]
18+
anchor-lang = "0.29.0"
19+
anchor-spl = "0.29.0"
20+
common = { path = "../common", features = ["no-entrypoint"] }
21+
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
3+
4+
declare_id!("Lottery1111111111111111111111111111");
5+
6+
#[account]
7+
pub struct LotteryTicket {
8+
pub player: Pubkey,
9+
pub numbers: [u8; 6],
10+
pub bet_amount: u64,
11+
pub draw_id: u64,
12+
pub timestamp: i64,
13+
pub bump: u8,
14+
}
15+
16+
impl LotteryTicket {
17+
pub const LEN: usize = 8 + 32 + 6 + 8 + 8 + 8 + 1;
18+
}
19+
20+
#[account]
21+
pub struct LotteryDraw {
22+
pub draw_id: u64,
23+
pub winning_numbers: [u8; 6],
24+
pub prize_pool: u64,
25+
pub tickets: u64,
26+
pub drawn: bool,
27+
pub timestamp: i64,
28+
pub bump: u8,
29+
}
30+
31+
impl LotteryDraw {
32+
pub const LEN: usize = 8 + 8 + 6 + 8 + 8 + 1 + 8 + 1;
33+
}
34+
35+
#[program]
36+
pub mod lottery {
37+
use super::*;
38+
39+
pub fn initialize(ctx: Context<Initialize>, min_bet: u64, max_bet: u64) -> Result<()> {
40+
let config = &mut ctx.accounts.config;
41+
config.authority = ctx.accounts.authority.key();
42+
config.treasury = ctx.accounts.treasury.key();
43+
config.min_bet = min_bet;
44+
config.max_bet = max_bet;
45+
config.house_edge_bps = 0; // No house edge for lottery
46+
config.paused = false;
47+
config.bump = ctx.bumps.config;
48+
Ok(())
49+
}
50+
51+
pub fn buy_ticket(ctx: Context<BuyTicket>, bet_amount: u64, numbers: [u8; 6], draw_id: u64) -> Result<()> {
52+
let config = &ctx.accounts.config;
53+
require!(!config.paused, common::CasinoError::GameNotActive);
54+
common::validate_bet(bet_amount, config.min_bet, config.max_bet)?;
55+
56+
// Validate numbers (1-49)
57+
for &num in &numbers {
58+
require!(num >= 1 && num <= 49, common::CasinoError::InvalidBetAmount);
59+
}
60+
61+
// Transfer bet to prize pool
62+
let cpi_accounts = Transfer {
63+
from: ctx.accounts.player_token_account.to_account_info(),
64+
to: ctx.accounts.pool_token_account.to_account_info(),
65+
authority: ctx.accounts.player.to_account_info(),
66+
};
67+
let cpi_program = ctx.accounts.token_program.to_account_info();
68+
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
69+
token::transfer(cpi_ctx, bet_amount)?;
70+
71+
// Update draw pool
72+
let draw = &mut ctx.accounts.draw;
73+
draw.prize_pool = draw.prize_pool
74+
.checked_add(bet_amount)
75+
.ok_or(common::CasinoError::MathOverflow)?;
76+
draw.tickets = draw.tickets
77+
.checked_add(1)
78+
.ok_or(common::CasinoError::MathOverflow)?;
79+
80+
// Create ticket
81+
let ticket = &mut ctx.accounts.ticket;
82+
ticket.player = ctx.accounts.player.key();
83+
ticket.numbers = numbers;
84+
ticket.bet_amount = bet_amount;
85+
ticket.draw_id = draw_id;
86+
ticket.timestamp = Clock::get()?.unix_timestamp;
87+
ticket.bump = ctx.bumps.ticket;
88+
89+
Ok(())
90+
}
91+
92+
pub fn draw_numbers(ctx: Context<DrawNumbers>) -> Result<()> {
93+
let draw = &mut ctx.accounts.draw;
94+
require!(!draw.drawn, common::CasinoError::GameAlreadySettled);
95+
96+
// Generate 6 random numbers (1-49)
97+
let seed = Clock::get()?.unix_timestamp.to_le_bytes();
98+
let mut numbers = [0u8; 6];
99+
let mut used = [false; 50];
100+
101+
for i in 0..6 {
102+
let mut num;
103+
loop {
104+
num = (common::generate_random_from_seed(&[seed[i]], 48) + 1) as u8;
105+
if !used[num as usize] {
106+
used[num as usize] = true;
107+
break;
108+
}
109+
}
110+
numbers[i] = num;
111+
}
112+
113+
numbers.sort();
114+
draw.winning_numbers = numbers;
115+
draw.drawn = true;
116+
draw.timestamp = Clock::get()?.unix_timestamp;
117+
118+
Ok(())
119+
}
120+
121+
pub fn claim_prize(ctx: Context<ClaimPrize>, matching_numbers: u8) -> Result<()> {
122+
let ticket = &ctx.accounts.ticket;
123+
let draw = &ctx.accounts.draw;
124+
require!(draw.drawn, common::CasinoError::InvalidGameState);
125+
require!(ticket.player == ctx.accounts.player.key(), common::CasinoError::Unauthorized);
126+
127+
// Calculate matches
128+
let mut matches = 0;
129+
for &num in &ticket.numbers {
130+
if draw.winning_numbers.contains(&num) {
131+
matches += 1;
132+
}
133+
}
134+
135+
require!(matches == matching_numbers, common::CasinoError::InvalidBetAmount);
136+
137+
// Calculate prize based on matches
138+
let prize = match matches {
139+
6 => draw.prize_pool, // Jackpot
140+
5 => draw.prize_pool / 10,
141+
4 => draw.prize_pool / 100,
142+
_ => 0,
143+
};
144+
145+
if prize > 0 {
146+
let seeds = &[
147+
b"pool",
148+
ctx.accounts.draw.to_account_info().key.as_ref(),
149+
&[draw.bump],
150+
];
151+
let signer = &[&seeds[..]];
152+
153+
let cpi_accounts = Transfer {
154+
from: ctx.accounts.pool_token_account.to_account_info(),
155+
to: ctx.accounts.player_token_account.to_account_info(),
156+
authority: ctx.accounts.draw.to_account_info(),
157+
};
158+
let cpi_program = ctx.accounts.token_program.to_account_info();
159+
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
160+
token::transfer(cpi_ctx, prize)?;
161+
}
162+
163+
Ok(())
164+
}
165+
}
166+
167+
#[derive(Accounts)]
168+
pub struct Initialize<'info> {
169+
#[account(
170+
init,
171+
payer = authority,
172+
space = common::GameConfig::LEN,
173+
seeds = [b"config", b"lottery"],
174+
bump
175+
)]
176+
pub config: Account<'info, common::GameConfig>,
177+
178+
#[account(mut)]
179+
pub authority: Signer<'info>,
180+
181+
/// CHECK: Treasury account
182+
pub treasury: UncheckedAccount<'info>,
183+
184+
pub system_program: Program<'info, System>,
185+
}
186+
187+
#[derive(Accounts)]
188+
pub struct BuyTicket<'info> {
189+
#[account(
190+
seeds = [b"config", b"lottery"],
191+
bump = config.bump
192+
)]
193+
pub config: Account<'info, common::GameConfig>,
194+
195+
#[account(mut)]
196+
pub player: Signer<'info>,
197+
198+
#[account(mut)]
199+
pub player_token_account: Account<'info, TokenAccount>,
200+
201+
#[account(mut)]
202+
pub pool_token_account: Account<'info, TokenAccount>,
203+
204+
#[account(
205+
init_if_needed,
206+
payer = player,
207+
space = LotteryDraw::LEN,
208+
seeds = [b"draw", &draw_id.to_le_bytes()],
209+
bump
210+
)]
211+
pub draw: Account<'info, LotteryDraw>,
212+
213+
#[account(
214+
init,
215+
payer = player,
216+
space = LotteryTicket::LEN,
217+
seeds = [b"ticket", player.key().as_ref(), &Clock::get()?.unix_timestamp.to_le_bytes()],
218+
bump
219+
)]
220+
pub ticket: Account<'info, LotteryTicket>,
221+
222+
pub token_program: Program<'info, Token>,
223+
pub system_program: Program<'info, System>,
224+
pub clock: Sysvar<'info, Clock>,
225+
}
226+
227+
#[derive(Accounts)]
228+
pub struct DrawNumbers<'info> {
229+
#[account(mut)]
230+
pub draw: Account<'info, LotteryDraw>,
231+
pub authority: Signer<'info>,
232+
pub clock: Sysvar<'info, Clock>,
233+
}
234+
235+
#[derive(Accounts)]
236+
pub struct ClaimPrize<'info> {
237+
#[account(mut)]
238+
pub ticket: Account<'info, LotteryTicket>,
239+
240+
#[account(mut)]
241+
pub draw: Account<'info, LotteryDraw>,
242+
243+
#[account(mut)]
244+
pub player: Signer<'info>,
245+
246+
#[account(mut)]
247+
pub player_token_account: Account<'info, TokenAccount>,
248+
249+
#[account(mut)]
250+
pub pool_token_account: Account<'info, TokenAccount>,
251+
252+
pub token_program: Program<'info, Token>,
253+
}
254+

0 commit comments

Comments
 (0)