Cairo Academy: Understanding a Basic Lottery Contract

This chapter delves into the basics of a lottery contract implemented in Cairo. This contract demonstrates the core functionalities of a basic lottery contract, providing a practical example for learning how to build a basic lottery contract on StarkNet.

Purpose and Functionality

The provided Cairo code defines a basic lottery contract. It implements essential functionalities such as:

  • Start Lottery: This starts the operation of a lottery round. This takes in parameters: ticket_price : Price of a single lottery ticket in wei max_tickets : Maximum number of tickets for this lottery round prize_distribution : Array containing percentage distribution of prizes (must sum to 100) duration : Duration of the lottery in seconds

  • Buy lottery tickets: Buy lottery tickets. This takes parameters: ticket_count : Number of tickets to purchase

  • End the lottery and select winners:. This takes parametrs: random_seed : Additional entropy source to enhance randomness

  • Claim prizes for the caller if they won

We have some view functions. These are functions that read data from the blockcahin.

  • Get lottery information : This get the lottery information
  • Get ticket holder : This gets the information about the lottery ticket holder
  • Get Winner : This gets the winner information
  • Get user tickets : This gets information about the user tickets
  • Is the lottery active This return a bool (true or flase) the active state of the lottery ticket

This contract serves as a foundational example for understanding the mechanics of Lottery Contract within the StarkNet ecosystem.

#![allow(unused)]
fn main() {
use starknet::ContractAddress;
use array::ArrayTrait;
use option::OptionTrait;

#[starknet::interface]
pub trait ILotteryContract<TContractState> {
    fn start_lottery(
        ref self: TContractState,
        ticket_price: u256,
        max_tickets: u32,
        prize_distribution: Array<u8>,
        duration: u64
    );
    fn buy_tickets(ref self: TContractState, ticket_count: u32);
    fn end_lottery(ref self: TContractState, random_seed: felt252);
    fn claim_prize(ref self: TContractState);
    fn get_lottery_info(self: @TContractState) -> LotteryInfo;
    fn get_ticket_holders(self: @TContractState) -> Array<ContractAddress>;
    fn get_winners(self: @TContractState) -> Array<Winner>;
    fn get_user_tickets(self: @TContractState, user: ContractAddress) -> u32;
    fn is_lottery_active(self: @TContractState) -> bool;
}

#[derive(Copy, Drop, Serde, starknet::Store)]
pub struct LotteryInfo {
    lottery_id: u32,
    ticket_price: u256,
    max_tickets: u32,
    tickets_sold: u32,
    prize_pool: u256,
    start_time: u64,
    end_time: u64,
    lottery_status: u8, 
}

#[derive(Copy, Drop, Serde)]
pub struct Winner {
    address: ContractAddress,
    prize_amount: u256,
    claimed: bool
}

mod Errors {
    pub const LOTTERY_ALREADY_ACTIVE: felt252 = 'Lottery already active';
    pub const INVALID_TICKET_PRICE: felt252 = 'Invalid ticket price';
    pub const INVALID_MAX_TICKETS: felt252 = 'Invalid max tickets';
    pub const INVALID_DURATION: felt252 = 'Invalid duration';
    pub const INVALID_DISTRIBUTION: felt252 = 'Invalid prize distribution';
    pub const LOTTERY_NOT_ACTIVE: felt252 = 'Lottery not active';
    pub const LOTTERY_NOT_ENDED: felt252 = 'Lottery still active';
    pub const LOTTERY_ALREADY_ENDED: felt252 = 'Lottery already ended';
    pub const MAX_TICKETS_REACHED: felt252 = 'Max tickets reached';
    pub const INSUFFICIENT_PAYMENT: felt252 = 'Insufficient payment';
    pub const NOT_OWNER: felt252 = 'Not the contract owner';
    pub const NO_PRIZES_TO_CLAIM: felt252 = 'No prizes to claim';
    pub const PRIZE_ALREADY_CLAIMED: felt252 = 'Prize already claimed';
}

#[starknet::contract]
pub mod LotteryContract {
    use super::{
        ILotteryContract, LotteryInfo, Winner, Errors,
        ArrayTrait, ContractAddress, OptionTrait
    };
    use core::num::traits::Zero;
    use starknet::{
        get_caller_address, get_block_timestamp, get_block_number, get_tx_info,
        get_contract_address
    };
    use openzeppelin_token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
    use core::hash::{LegacyHash, HashStateTrait};
    use core::pedersen::PedersenTrait;
    use core::traits::Into;
    use core::array::SpanTrait;

    #[storage]
    struct Storage {
        owner: ContractAddress,
        payment_token: IERC20Dispatcher,
        lottery_id: u32,
        active_lottery: LotteryInfo,
        ticket_holders: LegacyMap::<u32, ContractAddress>, 
        user_tickets: LegacyMap::<(u32, ContractAddress), u32>, count
        winners: LegacyMap::<(u32, u32), Winner>, 
        winner_count: LegacyMap::<u32, u32>, 
        prize_distribution: LegacyMap::<(u32, u32), u8>, 
        prize_distribution_count: LegacyMap::<u32, u32>, 
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        LotteryStarted: LotteryStarted,
        TicketsPurchased: TicketsPurchased,
        LotteryEnded: LotteryEnded,
        PrizeClaimed: PrizeClaimed,
    }

    #[derive(Drop, starknet::Event)]
    struct LotteryStarted {
        lottery_id: u32,
        ticket_price: u256,
        max_tickets: u32,
        duration: u64,
    }

    #[derive(Drop, starknet::Event)]
    struct TicketsPurchased {
        lottery_id: u32,
        buyer: ContractAddress,
        ticket_count: u32,
        total_cost: u256,
    }

    #[derive(Drop, starknet::Event)]
    struct LotteryEnded {
        lottery_id: u32,
        total_tickets_sold: u32,
        prize_pool: u256,
        timestamp: u64,
    }

    #[derive(Drop, starknet::Event)]
    struct PrizeClaimed {
        lottery_id: u32,
        winner: ContractAddress,
        prize_amount: u256,
    }

    #[constructor]
    fn constructor(
        ref self: ContractState,
        payment_token_address: ContractAddress,
    ) {
        self.owner.write(get_caller_address());
        self.payment_token.write(IERC20Dispatcher { contract_address: payment_token_address });
        self.lottery_id.write(0);
    }

    #[abi(embed_v0)]
    impl LotteryContractImpl of super::ILotteryContract<ContractState> {
        fn start_lottery(
            ref self: ContractState, 
            ticket_price: u256, 
            max_tickets: u32,
            prize_distribution: Array<u8>,
            duration: u64
        ) {
            self.assert_only_owner();
            
            assert(!self.is_lottery_active(@self), Errors::LOTTERY_ALREADY_ACTIVE);
            
            assert(ticket_price > 0, Errors::INVALID_TICKET_PRICE);
            assert(max_tickets > 0, Errors::INVALID_MAX_TICKETS);
            assert(duration > 0, Errors::INVALID_DURATION);
            
            let mut total_percentage: u8 = 0;
            let mut i: u32 = 0;
            let distribution_len = prize_distribution.len();
            assert(distribution_len > 0, Errors::INVALID_DISTRIBUTION);

            let distribution_span = prize_distribution.span();
            loop {
                if i >= distribution_len {
                    break;
                }
                let percentage = *distribution_span.at(i);
                total_percentage += percentage;
                self.prize_distribution.write((self.lottery_id.read() + 1, i), percentage);
                i += 1;
            }
            
            assert(total_percentage == 100, Errors::INVALID_DISTRIBUTION);
            self.prize_distribution_count.write(self.lottery_id.read() + 1, distribution_len);
            
            let new_lottery_id = self.lottery_id.read() + 1;
            let start_time = get_block_timestamp();
            let end_time = start_time + duration;
            
            self.active_lottery.write(LotteryInfo {
                lottery_id: new_lottery_id,
                ticket_price,
                max_tickets,
                tickets_sold: 0,
                prize_pool: 0,
                start_time,
                end_time,
                lottery_status: 1,
            });
            
            self.lottery_id.write(new_lottery_id);
            
            self.emit(LotteryStarted {
                lottery_id: new_lottery_id,
                ticket_price,
                max_tickets,
                duration,
            });
        }

        fn buy_tickets(ref self: ContractState, ticket_count: u32) {
            assert(self.is_lottery_active(@self), Errors::LOTTERY_NOT_ACTIVE);
            
            let caller = get_caller_address();
            let active_lottery = self.active_lottery.read();
            
            assert(active_lottery.tickets_sold + ticket_count <= active_lottery.max_tickets, Errors::MAX_TICKETS_REACHED);
            
            let total_cost = active_lottery.ticket_price * ticket_count.into();
            
            self.payment_token.read().transfer_from(
                caller, 
                get_contract_address(), 
                total_cost
            );
            
            let current_tickets_sold = active_lottery.tickets_sold;
        
            let mut i: u32 = 0;
            loop {
                if i >= ticket_count {
                    break;
                }
                self.ticket_holders.write(current_tickets_sold + i, caller);
                i += 1;
            }
            
            let user_current_tickets = self.user_tickets.read((active_lottery.lottery_id, caller));
            self.user_tickets.write((active_lottery.lottery_id, caller), user_current_tickets + ticket_count);
            
            self.active_lottery.write(LotteryInfo {
                lottery_id: active_lottery.lottery_id,
                ticket_price: active_lottery.ticket_price,
                max_tickets: active_lottery.max_tickets,
                tickets_sold: active_lottery.tickets_sold + ticket_count,
                prize_pool: active_lottery.prize_pool + total_cost,
                start_time: active_lottery.start_time,
                end_time: active_lottery.end_time,
                lottery_status: active_lottery.lottery_status,
            });
            
            self.emit(TicketsPurchased {
                lottery_id: active_lottery.lottery_id,
                buyer: caller,
                ticket_count,
                total_cost,
            });
        }

        fn end_lottery(ref self: ContractState, random_seed: felt252) {
            self.assert_only_owner();
            
            assert(self.is_lottery_active(@self), Errors::LOTTERY_NOT_ACTIVE);
            
            let active_lottery = self.active_lottery.read();
        
            assert(
                get_block_timestamp() >= active_lottery.end_time || 
                active_lottery.tickets_sold == active_lottery.max_tickets,
                Errors::LOTTERY_NOT_ENDED
            );
            
            if active_lottery.tickets_sold == 0 {
                self.active_lottery.write(LotteryInfo {
                    lottery_id: active_lottery.lottery_id,
                    ticket_price: active_lottery.ticket_price,
                    max_tickets: active_lottery.max_tickets,
                    tickets_sold: active_lottery.tickets_sold,
                    prize_pool: active_lottery.prize_pool,
                    start_time: active_lottery.start_time,
                    end_time: active_lottery.end_time,
                    lottery_status: 2, 
                });
                return;
            }

            let random_value = self.generate_random_value(random_seed);
            
            let prize_pool = active_lottery.prize_pool;
            let mut winner_count: u32 = 0;
            let distribution_count = self.prize_distribution_count.read(active_lottery.lottery_id);
            
            let mut i: u32 = 0;
            loop {
                if i >= distribution_count {
                    break;
                }
                
                let percentage = self.prize_distribution.read((active_lottery.lottery_id, i));
                let prize_amount = (prize_pool * percentage.into()) / 100_u256;
                
                if prize_amount > 0 {
                    let winner_ticket = self.select_random_ticket(
                        active_lottery.tickets_sold, 
                        random_value, 
                        i
                    );
                    let winner_address = self.ticket_holders.read(winner_ticket);
                    
                    self.winners.write(
                        (active_lottery.lottery_id, i),
                        Winner { 
                            address: winner_address, 
                            prize_amount,
                            claimed: false 
                        }
                    );
                    
                    winner_count += 1;
                }
                
                i += 1;
            }
            
            self.winner_count.write(active_lottery.lottery_id, winner_count);
            
            self.active_lottery.write(LotteryInfo {
                lottery_id: active_lottery.lottery_id,
                ticket_price: active_lottery.ticket_price,
                max_tickets: active_lottery.max_tickets,
                tickets_sold: active_lottery.tickets_sold,
                prize_pool: active_lottery.prize_pool,
                start_time: active_lottery.start_time,
                end_time: active_lottery.end_time,
                lottery_status: 2, 
            });
            
            self.emit(LotteryEnded {
                lottery_id: active_lottery.lottery_id,
                total_tickets_sold: active_lottery.tickets_sold,
                prize_pool: active_lottery.prize_pool,
                timestamp: get_block_timestamp(),
            });
        }

        fn claim_prize(ref self: ContractState) {
            let active_lottery = self.active_lottery.read();
            let caller = get_caller_address();
            
            assert(active_lottery.lottery_status >= 2, Errors::LOTTERY_NOT_ENDED);
            
            let lottery_id = active_lottery.lottery_id;
            let winner_count = self.winner_count.read(lottery_id);
            
            let mut prize_found = false;
            let mut total_prize: u256 = 0;
            let mut i: u32 = 0;
            
            loop {
                if i >= winner_count {
                    break;
                }
                
                let winner = self.winners.read((lottery_id, i));
                if winner.address == caller && !winner.claimed {
                    prize_found = true;
                    total_prize += winner.prize_amount;
                    
                    self.winners.write(
                        (lottery_id, i),
                        Winner { 
                            address: winner.address, 
                            prize_amount: winner.prize_amount,
                            claimed: true 
                        }
                    );
                }
                
                i += 1;
            }
            
            assert(prize_found, Errors::NO_PRIZES_TO_CLAIM);
            assert(total_prize > 0, Errors::NO_PRIZES_TO_CLAIM);
            
            self.payment_token.read().transfer(caller, total_prize);
            
            self.emit(PrizeClaimed {
                lottery_id,
                winner: caller,
                prize_amount: total_prize,
            });
        }

        fn get_lottery_info(self: @ContractState) -> LotteryInfo {
            self.active_lottery.read()
        }

        fn get_ticket_holders(self: @ContractState) -> Array<ContractAddress> {
            let active_lottery = self.active_lottery.read();
            let mut holders = ArrayTrait::new();
            let mut i: u32 = 0;
            
            loop {
                if i >= active_lottery.tickets_sold {
                    break;
                }
                
                holders.append(self.ticket_holders.read(i));
                i += 1;
            }
            
            holders
        }

        fn get_winners(self: @ContractState) -> Array<Winner> {
            let active_lottery = self.active_lottery.read();
            let lottery_id = active_lottery.lottery_id;
            let winner_count = self.winner_count.read(lottery_id);
            let mut winners = ArrayTrait::new();
            let mut i: u32 = 0;
            
            loop {
                if i >= winner_count {
                    break;
                }
                
                winners.append(self.winners.read((lottery_id, i)));
                i += 1;
            }
            
            winners
        }

        fn get_user_tickets(self: @ContractState, user: ContractAddress) -> u32 {
            let active_lottery = self.active_lottery.read();
            self.user_tickets.read((active_lottery.lottery_id, user))
        }

        fn is_lottery_active(self: @ContractState) -> bool {
            let active_lottery = self.active_lottery.read();
            active_lottery.lottery_status == 1
        }
    }

    #[generate_trait]
    impl PrivateFunctions of PrivateFunctionsTrait {
        fn assert_only_owner(self: @ContractState) {
            let caller = get_caller_address();
            let owner = self.owner.read();
            assert(caller == owner, Errors::NOT_OWNER);
        }

        fn generate_random_value(self: @ContractState, seed: felt252) -> felt252 {
            let block_number = get_block_number();
            let block_timestamp = get_block_timestamp();
            let tx_info = get_tx_info().unbox();
            let tx_hash = tx_info.transaction_hash;
            
            let mut hash_state = PedersenTrait::new(0);
            hash_state = hash_state.update(seed);
            hash_state = hash_state.update(block_number.into());
            hash_state = hash_state.update(block_timestamp.into());
            hash_state = hash_state.update(tx_hash);
            
            hash_state.finalize()
        }

        fn select_random_ticket(
            self: @ContractState,
            total_tickets: u32,
            random_value: felt252,
            offset: u32
        ) -> u32 {
    
            let mut hash_state = PedersenTrait::new(0);
            hash_state = hash_state.update(random_value);
            hash_state = hash_state.update(offset.into());
            let new_random = hash_state.finalize();
            
            (new_random.try_into().unwrap() % total_tickets.into()).try_into().unwrap()
        }
    }
}
}