Rust-based Solana programs handle ticket sales and winner selection. Open-source code ensures complete transparency and security.
Please check official GitHub for most recent updates. Official Links
Overview
This smart contract implements a secure and transparent raffle system on the Solana blockchain. It enables us to create raffles where participants can purchase tickets for chances to win prizes. The system prioritizes security, fairness, and transparency throughout the entire process.
Core Components
1. Raffle Creation and Management
The system allows authorized users to create and manage raffles with customizable parameters.
For Users:
Set ticket prices (0.1 to 100 SOL)
Define minimum required ticket sales
Set optional maximum ticket limit
Set raffle duration (1 hour to 30 days)
Provide prize information via metadata URI
Technical Implementation:
#[account]
pub struct Raffle {
pub authority: Pubkey, // Raffle creator/admin
pub treasury: Pubkey, // Treasury PDA holding funds
pub metadata_uri: String, // Prize metadata
pub ticket_price: u64, // Price per ticket in lamports
pub current_tickets: u64, // Total tickets sold
pub min_tickets: u64, // Minimum required sales
pub max_tickets: Option<u64>, // Optional maximum tickets limit
pub current_entries: u64, // Number of purchase transactions
pub creation_time: i64, // Unix timestamp of creation
pub end_time: i64, // Unix timestamp of end
pub raffle_state: RaffleState, // Current state
pub winner_address: Option<Pubkey>, // Winner if drawn
pub winning_ticket: Option<u64>, // The winning ticket that was drafted
}
pub fn create_raffle(
ctx: Context<CreateRaffle>,
metadata_uri: String,
ticket_price: u64,
end_time: i64,
min_tickets: u64,
max_tickets: Option<u64>,
) -> Result<()> {
let current_time = Clock::get()?.unix_timestamp;
// Validate inputs
// URI format check - must start with one of the valid prefixes
require!(
VALID_URI_PREFIXES
.iter()
.any(|prefix| metadata_uri.starts_with(prefix)),
RaffleError::InvalidMetadataUri
);
require!(metadata_uri.len() <= 256, RaffleError::MetadataUriTooLong);
// Price checks
require!(
ticket_price >= MIN_TICKET_PRICE,
RaffleError::TicketPriceTooLow
);
require!(
ticket_price <= MAX_TICKET_PRICE,
RaffleError::TicketPriceTooHigh
);
// Ticket count checks
require!(min_tickets > 0, RaffleError::MinTicketsTooLow);
require!(
min_tickets <= MAX_MIN_TICKETS,
RaffleError::MinTicketsTooHigh
);
// Check that max tickets is greater than or equal to min tickets
if let Some(max_tickets) = max_tickets {
require!(max_tickets >= min_tickets, RaffleError::MaxTicketsTooLow);
}
// Time checks
require!(
end_time > current_time.checked_add(MIN_DURATION).unwrap(),
RaffleError::EndTimeTooClose
);
require!(
end_time <= current_time.checked_add(MAX_DURATION).unwrap(),
RaffleError::DurationTooLong
);
// Set inputs from transaction data
ctx.accounts.raffle.metadata_uri = metadata_uri;
ctx.accounts.raffle.ticket_price = ticket_price;
ctx.accounts.raffle.min_tickets = min_tickets;
ctx.accounts.raffle.end_time = end_time;
ctx.accounts.raffle.treasury = ctx.accounts.treasury.key();
ctx.accounts.treasury.bump = ctx.bumps.treasury;
ctx.accounts.treasury.raffle = ctx.accounts.raffle.key();
ctx.accounts.raffle.max_tickets = max_tickets;
// Set default values
ctx.accounts.raffle.current_tickets = 0;
ctx.accounts.raffle.current_entries = 0;
ctx.accounts.raffle.creation_time = current_time;
ctx.accounts.raffle.raffle_state = RaffleState::Open;
ctx.accounts.raffle.winner_address = None;
ctx.accounts.raffle.winning_ticket = None;
// Increment the raffle counter
ctx.accounts.config.raffle_counter = ctx
.accounts
.config
.raffle_counter
.checked_add(1)
.ok_or(RaffleError::Overflow)?;
// Emit the raffle created event
emit!(RaffleCreated {
raffle: ctx.accounts.raffle.key(),
metadata_uri: ctx.accounts.raffle.metadata_uri.clone(),
ticket_price,
min_tickets,
end_time,
creation_time: current_time,
});
Ok(())
}
2. Ticket System
The ticket system manages purchases, ownership tracking, and ticket distribution.
For Users:
Purchase multiple tickets in a single transaction
View ticket ownership and balance
Track all transactions on-chain
Secure entry tracking with randomized seeds
Technical Implementation:
pub fn buy_tickets(ctx: Context<BuyTickets>, ticket_count: u64, entry_seed: [u8; 8]) -> Result<()> {
// Validate ticket count
require!(ticket_count > 0, RaffleError::InvalidTicketCount);
// Check if still allowed to buy tickets
if let Some(max_tickets) = ctx.accounts.raffle.max_tickets {
require!(
ctx.accounts.raffle.current_tickets < max_tickets,
RaffleError::MaximumTicketsSold
);
require!(
ctx.accounts.raffle.max_tickets >= ctx.accounts.raffle.current_tickets.checked_add(ticket_count),
RaffleError::PurchaseExceedsThreshold
);
}
// Calculate payment amount with overflow protection
let payment_amount = ticket_count
.checked_mul(ctx.accounts.raffle.ticket_price)
.ok_or(RaffleError::Overflow)?;
// Validate buyer has sufficient funds using checked comparison
require!(
ctx.accounts.signer.lamports()
.checked_sub(payment_amount)
.ok_or(RaffleError::InsufficientFunds)? > 0,
RaffleError::InsufficientFunds,
);
// Ensure treasury account matches the one stored in raffle
require!(
ctx.accounts.treasury.key() == ctx.accounts.raffle.treasury.key(),
RaffleError::InvalidTreasury,
);
// Verify ticket balance account is initialized
require!(
ctx.accounts.ticket_balance.owner == ctx.accounts.signer.key(),
RaffleError::TicketBalanceNotInitialized,
);
// Initialize entry data in the PDA
// Each entry represents a single purchase transaction
let entry = &mut ctx.accounts.entry;
entry.raffle = ctx.accounts.raffle.key();
entry.owner = ctx.accounts.signer.key();
entry.ticket_count = ticket_count;
entry.ticket_start_index = ctx.accounts.raffle.current_tickets;
entry.seed = entry_seed;
// Update raffle state with new ticket count using checked arithmetic
ctx.accounts.raffle.current_tickets = ctx.accounts.raffle.current_tickets
.checked_add(ticket_count)
.ok_or(RaffleError::Overflow)?;
// Update user's total ticket balance with overflow protection
let ticket_balance = &mut ctx.accounts.ticket_balance;
ticket_balance.ticket_count = ticket_balance.ticket_count
.checked_add(ticket_count)
.ok_or(RaffleError::Overflow)?;
// Store pre-transfer balance for verification
let pre_transfer_balance = ctx.accounts.treasury.to_account_info().lamports();
// Transfer lamports from the buyer to the raffle treasury
anchor_lang::solana_program::program::invoke(
&anchor_lang::solana_program::system_instruction::transfer(
&ctx.accounts.signer.key(),
&ctx.accounts.treasury.key(),
payment_amount,
),
&[
ctx.accounts.signer.to_account_info(),
ctx.accounts.system_program.to_account_info(),
ctx.accounts.treasury.to_account_info(),
],
)?;
// Verify the transfer was successful by checking treasury balance
let post_transfer_balance = ctx.accounts.treasury.to_account_info().lamports();
require!(
post_transfer_balance == pre_transfer_balance.checked_add(payment_amount).ok_or(RaffleError::Overflow)?,
RaffleError::TransferFailed
);
// Emit the tickets purchased event
emit!(TicketsPurchased {
raffle: ctx.accounts.raffle.key(),
buyer: ctx.accounts.signer.key(),
ticket_count,
payment_amount,
ticket_start_index: entry.ticket_start_index,
entry_seed,
});
Ok(())
}
3. Treasury Management
Secure handling of funds throughout the raffle lifecycle.
For Users:
Automatic fund collection in treasury
Secure withdrawal process
Automatic refunds for expired raffles
Protected balance tracking
Treasury can only be managed via program instructions (No associated private key)
Technical Implementation:
#[account]
pub struct Treasury {
pub raffle: Pubkey,
pub bump: u8,
}
#[account(
init,
payer = management_authority,
space = TREASURY_ACCOUNT_SIZE,
seeds = [
b"treasury",
raffle.key().as_ref(),
],
bump,
)]
pub treasury: Account<'info, Treasury>,
pub fn withdraw_from_treasury(ctx: Context<WithdrawFromTreasury>) -> Result<()> {
// Verify that the threshold has been met
require!(
ctx.accounts.raffle.current_tickets >= ctx.accounts.raffle.min_tickets,
RaffleError::ThresholdNotMet,
);
// Verify treasury account matches the one stored in raffle
require!(
ctx.accounts.treasury.key() == ctx.accounts.raffle.treasury,
RaffleError::InvalidTreasury
);
let treasury_account = ctx.accounts.treasury.to_account_info();
let payout_authority = ctx.accounts.payout_authority.to_account_info();
// Get total balance including rent
let treasury_balance = treasury_account.lamports();
require!(treasury_balance > 0, RaffleError::InsufficientFunds);
// Get rent exempt balance to make sure we don't deduct ALL lamports, as the raffle might still be open
let rent_lamports = (Rent::get()?).minimum_balance(TREASURY_ACCOUNT_SIZE);
let lamports_to_withdraw = treasury_balance - rent_lamports;
// Transfer lamports by directly deducting from treasury and adding to payout_authority.
// This only works because the treasury is a PDA owned by our program.
treasury_account.sub_lamports(lamports_to_withdraw)?;
payout_authority.add_lamports(lamports_to_withdraw)?;
// Emit the treasury withdrawn event
emit!(TreasuryWithdrawn {
raffle: ctx.accounts.raffle.key(),
amount: lamports_to_withdraw,
});
Ok(())
}
Winner Selection System
1. Randomness Implementation
The system uses a sophisticated multi-step process to ensure fair and verifiable winner selection.
For Users:
Transparent randomness source
Verifiable drawing process
Equal chance for every ticket
Resistant to manipulation
Technical Implementation:
pub fn draw_winning_ticket(ctx: Context<DrawWinningTicket>) -> Result<()> {
// Manually validate the recent_slothashes account
let pubkey_matches = Pubkey::from_str("SysvarS1otHashes111111111111111111111111111")
.or(Err(RaffleError::InvalidSlotHashesAccount))?
.eq(&ctx.accounts.recent_slothashes.key());
require!(pubkey_matches, RaffleError::InvalidSlotHashesAccount);
let recent_slothashes = &ctx.accounts.recent_slothashes;
let data = recent_slothashes.data.borrow();
// Extract entropy from SlotHashes data
let chunk1 = array_ref![data, 12, 8];
let chunk2 = if data.len() >= 28 {
// Get second 8-byte block if available
array_ref![data, 20, 8]
} else {
// Otherwise use the first block again
chunk1
};
let hash_value1 = u64::from_le_bytes(*chunk1);
let hash_value2 = u64::from_le_bytes(*chunk2);
let clock = Clock::get()?;
let timestamp = clock.unix_timestamp as u64;
// Combine entropy sources through cryptographic mixing
let mut mixed_value = mix(hash_value1, timestamp);
mixed_value = mix(mixed_value, hash_value2);
// Map the random value to a ticket number without statistical bias
let winning_ticket = unbiased_range(mixed_value, ctx.accounts.raffle.current_tickets)?;
// Store winning ticket and update state
ctx.accounts.raffle.winning_ticket = Some(winning_ticket);
ctx.accounts.raffle.raffle_state = RaffleState::Drawing;
Ok(())
}
/// Cryptographic mixing function with strong avalanche properties
/// Each bit in the output has a ~50% chance of flipping when any input bit changes.
/// Based on splitmix64 algorithm used in high-quality PRNGs.
fn mix(a: u64, b: u64) -> u64 {
let mut z = a.wrapping_add(b);
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
z = z ^ (z >> 31);
z
}
/// Maps a random number to a range without introducing statistical bias
/// Standard modulo operations can bias results when the range isn't a power of 2.
/// This function uses specialized techniques based on range size to ensure fairness.
fn unbiased_range(x: u64, range: u64) -> Result<u64> {
if range == 0 {
return Err(RaffleError::Overflow.into());
}
// If range is a power of 2, we can use a simple mask which is unbiased
if range.is_power_of_two() {
return Ok(x & (range - 1));
}
// For small ranges, simple modulo is fine as bias is minimal
if range <= 256 {
return Ok(x % range);
}
// Find threshold value to ensure unbiased selection
let threshold = u64::MAX - (u64::MAX % range);
// Use rejection sampling with a limit on computational cost
let mut value = x;
const MAX_ATTEMPTS: u8 = 3;
for i in 0..MAX_ATTEMPTS {
// If value is below threshold, we can use modulo safely
if value < threshold {
return Ok(value % range);
}
// Try a new value with additional mixing
value = mix(value, value.wrapping_add(i as u64 + 1));
}
// Fallback case - the bias is minimal after the mixing operations
Ok(value % range)
}
2. Winner Determination
Once a winning ticket is drawn, the system identifies the owner and updates the raffle state.
Technical Implementation:
pub fn set_winner(ctx: Context<SetWinner>, _entry_seed: [u8; 8]) -> Result<()> {
// Get the winning ticket number
let winning_ticket = ctx
.accounts
.raffle
.winning_ticket
.ok_or(RaffleError::NoWinningTicket)?;
// Verify the entry contains the winning ticket
let entry = &ctx.accounts.entry;
require!(
winning_ticket >= entry.ticket_start_index
&& winning_ticket
< entry
.ticket_start_index
.checked_add(entry.ticket_count)
.ok_or(RaffleError::Overflow)?,
RaffleError::InvalidWinningEntry
);
// Set the winner and update state
ctx.accounts.raffle.winner_address = Some(entry.owner);
ctx.accounts.raffle.raffle_state = RaffleState::Drawn;
// Emit winner set event
emit!(WinnerSet {
raffle: ctx.accounts.raffle.key(),
winner: entry.owner,
winning_ticket,
});
Ok(())
}
Security Features
1. Input Validation
The system implements strict validation for all inputs to prevent exploitation.
Multiple layers of protection for financial operations.
For Users:
Protected fund transfers
Balance verification
Overflow protection
Automated treasury management
Technical Implementation:
// Safe arithmetic operations
let payment_amount = ticket_count
.checked_mul(ctx.accounts.raffle.ticket_price)
.ok_or(RaffleError::Overflow)?;
// Prevent withdrawal of rent to ensure account remains valid
let rent_lamports = (Rent::get()?).minimum_balance(TREASURY_ACCOUNT_SIZE);
let lamports_to_withdraw = treasury_balance - rent_lamports;
// Transfer verification
let pre_transfer_balance = ctx.accounts.treasury.to_account_info().lamports();
// ... perform transfer ...
let post_transfer_balance = ctx.accounts.treasury.to_account_info().lamports();
require!(
post_transfer_balance == pre_transfer_balance
.checked_add(payment_amount)
.ok_or(RaffleError::Overflow)?,
RaffleError::TransferFailed
);
4. Enhanced Randomness
The system uses a sophisticated randomness implementation to ensure fair and unpredictable selection.
For Users:
Multiple entropy sources
Cryptographic mixing
Unbiased selection
Statistical fairness
Technical Implementation:
// Extract multiple entropy sources
let chunk1 = array_ref![data, 12, 8];
let chunk2 = if data.len() >= 28 {
array_ref![data, 20, 8]
} else {
chunk1
};
let hash_value1 = u64::from_le_bytes(*chunk1);
let hash_value2 = u64::from_le_bytes(*chunk2);
let timestamp = clock.unix_timestamp as u64;
// Apply cryptographic mixing
let mut mixed_value = mix(hash_value1, timestamp);
mixed_value = mix(mixed_value, hash_value2);
// Generate unbiased random number
let winning_ticket = unbiased_range(mixed_value, ctx.accounts.raffle.current_tickets)?;
Refund System
The contract includes functionality for users to reclaim funds from expired raffles.
For Users:
Automatic eligibility for refunds if raffle expires
Full refund of ticket purchase amount
Secure transfer process
Technical Implementation:
pub fn reclaim_expired_tickets(ctx: Context<ReclaimExpiredTickets>) -> Result<()> {
require!(
ctx.accounts.raffle.raffle_state == RaffleState::Expired,
RaffleError::RaffleNotExpired
);
require!(
ctx.accounts.signer.key() == ctx.accounts.ticket_balance.owner,
RaffleError::OwnerMismatch
);
require!(
ctx.accounts.raffle.treasury.key() == ctx.accounts.treasury.key(),
RaffleError::InvalidTreasury
);
require!(
ctx.accounts.ticket_balance.ticket_count > 0,
RaffleError::NoTicketsOwned
);
let from_pubkey = ctx.accounts.treasury.to_account_info();
let to_pubkey = ctx.accounts.signer.to_account_info();
// Transfer lamports by directly deducting from treasury and adding to signer.
// This only works because the treasury is a PDA owned by our program.
let total_lamports_to_transfer = ctx.accounts.ticket_balance.ticket_count * ctx.accounts.raffle.ticket_price;
from_pubkey.sub_lamports(total_lamports_to_transfer)?;
to_pubkey.add_lamports(total_lamports_to_transfer)?;
Ok(())
}
State Management
Raffle States
The system maintains clear state transitions throughout the raffle lifecycle.
For Users:
Open: Active and accepting tickets
Drawing: Winning ticket drawn, looking for entry with ticket
The contract emits events for important actions to provide transparency and auditability.
Technical Implementation:
/// Event emitted when a raffle is created
#[event]
pub struct RaffleCreated {
/// The pubkey of the created raffle
pub raffle: Pubkey,
/// The metadata URI for the raffle
pub metadata_uri: String,
/// Price per ticket in lamports
pub ticket_price: u64,
/// Minimum number of tickets required
pub min_tickets: u64,
/// When the raffle ends
pub end_time: i64,
/// When the raffle was created
pub creation_time: i64,
}
/// Event emitted when tickets are purchased
#[event]
pub struct TicketsPurchased {
/// The pubkey of the raffle
pub raffle: Pubkey,
/// The buyer's address
pub buyer: Pubkey,
/// Number of tickets purchased
pub ticket_count: u64,
/// Total amount paid in lamports
pub payment_amount: u64,
/// Starting ticket index for this purchase
pub ticket_start_index: u64,
/// The seed that was used to create the entry
pub entry_seed: [u8; 8],
}
/// Event emitted when a winner is set for a raffle
#[event]
pub struct WinnerSet {
/// The pubkey of the raffle
pub raffle: Pubkey,
/// The winner's address
pub winner: Pubkey,
/// The winning ticket number
pub winning_ticket: u64,
}
/// Event emitted when a winner submits their encrypted data
#[event]
pub struct WinnerDataSubmitted {
/// The pubkey of the raffle
pub raffle: Pubkey,
}
/// Event emitted when treasury funds are withdrawn
#[event]
pub struct TreasuryWithdrawn {
/// The pubkey of the raffle
pub raffle: Pubkey,
/// Amount withdrawn in lamports
pub amount: u64,
}
/// Event emitted when a raffle is expired
#[event]
pub struct RaffleExpired {
/// The pubkey of the expired raffle
pub raffle: Pubkey,
/// The timestamp when the raffle was expired
pub expired_at: i64,
/// The final number of tickets sold
pub final_ticket_count: u64,
}
Error Handling System
Error Definitions
The contract implements comprehensive error handling for all operations.
For Users:
Clear error messages
Specific error types
Input validation errors
State transition errors
Financial operation errors
Technical Implementation:
#[error_code]
pub enum RaffleError {
#[msg("Raffle is not in Open state")]
RaffleNotOpen,
#[msg("Raffle is not in Drawing state")]
RaffleNotDrawing,
#[msg("Raffle is not in Drawn state")]
RaffleNotDrawn,
#[msg("Raffle is not in Expired state")]
RaffleNotExpired,
#[msg("Raffle has not ended yet")]
RaffleNotEnded,
#[msg("Raffle has already ended")]
RaffleEnded,
#[msg("Ticket price is too low")]
TicketPriceTooLow,
#[msg("Ticket price is too high")]
TicketPriceTooHigh,
#[msg("Minimum tickets threshold is too low")]
MinTicketsTooLow,
#[msg("Minimum tickets threshold is too high")]
MinTicketsTooHigh,
#[msg("Maximum tickets is too low")]
MaxTicketsTooLow,
#[msg("Maximum tickets have been sold")]
MaximumTicketsSold,
#[msg("Purchase exceeds maximum ticket threshold")]
PurchaseExceedsThreshold,
#[msg("End time is too close to current time")]
EndTimeTooClose,
#[msg("Raffle duration is too long")]
DurationTooLong,
#[msg("Invalid metadata URI")]
InvalidMetadataUri,
#[msg("Metadata URI is too long")]
MetadataUriTooLong,
#[msg("Invalid ticket count")]
InvalidTicketCount,
#[msg("Insufficient funds")]
InsufficientFunds,
#[msg("Invalid treasury account")]
InvalidTreasury,
#[msg("Transfer failed")]
TransferFailed,
#[msg("Arithmetic overflow")]
Overflow,
#[msg("Minimum ticket threshold has not been met")]
InsufficientTickets,
#[msg("Minimum ticket threshold has not been met")]
ThresholdNotMet,
#[msg("Minimum ticket threshold has been met")]
ThresholdIsMet,
#[msg("Invalid SlotHashes account")]
InvalidSlotHashesAccount,
#[msg("No winning ticket has been drawn")]
NoWinningTicket,
#[msg("Invalid winning entry")]
InvalidWinningEntry,
#[msg("Owner mismatch")]
OwnerMismatch,
#[msg("No tickets owned")]
NoTicketsOwned,
#[msg("Ticket balance not initialized")]
TicketBalanceNotInitialized,
#[msg("Config not initialized")]
ConfigNotInitialized,
#[msg("Not program management authority")]
NotProgramManagementAuthority,
#[msg("Not program payout authority")]
NotPayoutAuthority,
#[msg("Not the winner")]
NotWinner,
#[msg("Invalid data length")]
InvalidDataLength,
}