Source Code

;; Daily Raffle Smart Contract
;; A decentralized lottery where users buy tickets and a winner takes the pot

;; ============================================
;; Constants
;; ============================================

;; Ticket price: 1 STX (1,000,000 microSTX)
(define-constant TICKET-PRICE u1000000)

;; Contract owner (deployer)
(define-constant CONTRACT-OWNER tx-sender)

;; Dev fee percentage (5% = 500 basis points)
(define-constant DEV-FEE-BPS u500)
(define-constant BPS-DENOMINATOR u10000)

;; Minimum blocks before drawing is allowed (prevents same-block manipulation)
(define-constant MIN-BLOCKS-BEFORE-DRAW u10)

;; ============================================
;; Error Codes
;; ============================================

(define-constant ERR-NOT-OWNER (err u100))
(define-constant ERR-NO-TICKETS-SOLD (err u101))
(define-constant ERR-TRANSFER-FAILED (err u102))
(define-constant ERR-ALREADY-DRAWN (err u103))
(define-constant ERR-TOO-EARLY-TO-DRAW (err u104))
(define-constant ERR-INVALID-TICKET-ID (err u105))
(define-constant ERR-ROUND-NOT-FOUND (err u106))
(define-constant ERR-NO-PRIZE-TO-CLAIM (err u107))
(define-constant ERR-ALREADY-CLAIMED (err u108))

;; ============================================
;; Data Variables
;; ============================================

;; Current round number
(define-data-var current-round uint u1)

;; Block height when round started (initialized to 0, set on first action)
(define-data-var round-start-block uint u0)

;; ============================================
;; Data Maps
;; ============================================

;; Map ticket-id to owner for each round
(define-map Tickets
  { round: uint, ticket-id: uint }
  { owner: principal }
)

;; Round information
(define-map Round-Info
  uint
  {
    tickets-sold: uint,
    unique-players: uint,
    pot-balance: uint,
    winner: (optional principal),
    is-drawn: bool,
    draw-block: uint,
    prize-amount: uint,
    is-claimed: bool
  }
)

;; User tickets per round
(define-map User-Tickets
  { round: uint, user: principal }
  { count: uint }
)

;; Unclaimed prizes for winners
(define-map Unclaimed-Prizes
  principal
  { amount: uint, round: uint }
)

;; ============================================
;; Private Functions
;; ============================================

;; Get current round info or default
(define-private (get-current-round-info)
  (default-to
    {
      tickets-sold: u0,
      unique-players: u0,
      pot-balance: u0,
      winner: none,
      is-drawn: false,
      draw-block: u0,
      prize-amount: u0,
      is-claimed: false
    }
    (map-get? Round-Info (var-get current-round))
  )
)

;; Calculate dev fee from pot
(define-private (calculate-dev-fee (amount uint))
  (/ (* amount DEV-FEE-BPS) BPS-DENOMINATOR)
)

;; Generate pseudo-random number using block hash
(define-private (get-random-seed (target-block uint))
  (match (get-burn-block-info? header-hash target-block)
    hash-value (some (buff-to-uint hash-value))
    none
  )
)

;; Convert 32-byte buffer to uint using first 8 bytes (big-endian)
(define-private (buff-to-uint (hash (buff 32)))
  (+
    (* (buff-to-u8 (unwrap-panic (element-at hash u0))) u72057594037927936)
    (* (buff-to-u8 (unwrap-panic (element-at hash u1))) u281474976710656)
    (* (buff-to-u8 (unwrap-panic (element-at hash u2))) u1099511627776)
    (* (buff-to-u8 (unwrap-panic (element-at hash u3))) u4294967296)
    (* (buff-to-u8 (unwrap-panic (element-at hash u4))) u16777216)
    (* (buff-to-u8 (unwrap-panic (element-at hash u5))) u65536)
    (* (buff-to-u8 (unwrap-panic (element-at hash u6))) u256)
    (buff-to-u8 (unwrap-panic (element-at hash u7)))
  )
)

;; Convert single byte to uint
(define-private (buff-to-u8 (byte (buff 1)))
  (unwrap-panic (index-of 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff byte))
)

;; ============================================
;; Public Functions
;; ============================================

;; Buy a ticket for the current round
(define-public (buy-ticket)
  (let
    (
      (round (var-get current-round))
      (round-info (get-current-round-info))
      (new-ticket-id (+ (get tickets-sold round-info) u1))
      (user-ticket-info (default-to { count: u0 } (map-get? User-Tickets { round: round, user: tx-sender })))
      (is-new-player (is-eq (get count user-ticket-info) u0))
    )
    (try! (stx-transfer? TICKET-PRICE tx-sender current-contract))
    
    (map-set Tickets
      { round: round, ticket-id: new-ticket-id }
      { owner: tx-sender }
    )
    
    (map-set Round-Info round
      {
        tickets-sold: new-ticket-id,
        unique-players: (if is-new-player 
                          (+ (get unique-players round-info) u1) 
                          (get unique-players round-info)),
        pot-balance: (+ (get pot-balance round-info) TICKET-PRICE),
        winner: none,
        is-drawn: false,
        draw-block: u0,
        prize-amount: u0,
        is-claimed: false
      }
    )
    
    (map-set User-Tickets
      { round: round, user: tx-sender }
      { count: (+ (get count user-ticket-info) u1) }
    )
    
    (ok { ticket-id: new-ticket-id, round: round })
  )
)

;; Buy multiple tickets at once
(define-public (buy-tickets (quantity uint))
  (let
    (
      (round (var-get current-round))
      (round-info (get-current-round-info))
      (total-cost (* TICKET-PRICE quantity))
      (start-ticket-id (+ (get tickets-sold round-info) u1))
      (user-ticket-info (default-to { count: u0 } (map-get? User-Tickets { round: round, user: tx-sender })))
      (is-new-player (is-eq (get count user-ticket-info) u0))
    )
    (try! (stx-transfer? total-cost tx-sender current-contract))
    
    (fold register-ticket-fold
      (list u1 u2 u3 u4 u5 u6 u7 u8 u9 u10)
      { start-id: start-ticket-id, round: round, quantity: quantity, registered: u0 }
    )
    
    (map-set Round-Info round
      {
        tickets-sold: (+ (get tickets-sold round-info) quantity),
        unique-players: (if is-new-player 
                          (+ (get unique-players round-info) u1) 
                          (get unique-players round-info)),
        pot-balance: (+ (get pot-balance round-info) total-cost),
        winner: none,
        is-drawn: false,
        draw-block: u0,
        prize-amount: u0,
        is-claimed: false
      }
    )
    
    (map-set User-Tickets
      { round: round, user: tx-sender }
      { count: (+ (get count user-ticket-info) quantity) }
    )
    
    (ok { tickets-bought: quantity, round: round })
  )
)

;; Helper fold function to register multiple tickets
(define-private (register-ticket-fold 
  (index uint) 
  (state { start-id: uint, round: uint, quantity: uint, registered: uint })
)
  (if (< (get registered state) (get quantity state))
    (begin
      (map-set Tickets
        { round: (get round state), ticket-id: (+ (get start-id state) (get registered state)) }
        { owner: tx-sender }
      )
      { 
        start-id: (get start-id state), 
        round: (get round state), 
        quantity: (get quantity state), 
        registered: (+ (get registered state) u1) 
      }
    )
    state
  )
)

;; Draw the winner for the current round (only owner can call)
(define-public (draw-winner)
  (let
    (
      (round (var-get current-round))
      (round-info (get-current-round-info))
      (tickets-sold (get tickets-sold round-info))
      (unique-players (get unique-players round-info))
      (pot-balance (get pot-balance round-info))
      (blocks-elapsed (- stacks-block-height (var-get round-start-block)))
    )
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER)
    (asserts! (> tickets-sold u0) ERR-NO-TICKETS-SOLD)
    (asserts! (not (get is-drawn round-info)) ERR-ALREADY-DRAWN)
    (asserts! (>= blocks-elapsed MIN-BLOCKS-BEFORE-DRAW) ERR-TOO-EARLY-TO-DRAW)
    
    (let
      (
        (random-seed (unwrap! (get-random-seed (- burn-block-height u1)) ERR-ROUND-NOT-FOUND))
        (winning-ticket-id (+ (mod random-seed tickets-sold) u1))
        (winner-data (unwrap! (map-get? Tickets { round: round, ticket-id: winning-ticket-id }) ERR-INVALID-TICKET-ID))
        (winner (get owner winner-data))
        (dev-fee (calculate-dev-fee pot-balance))
        (winner-prize (- pot-balance dev-fee))
      )
      
      (try! (as-contract? ((with-stx dev-fee)) (unwrap-panic (stx-transfer? dev-fee tx-sender CONTRACT-OWNER))))
      
      (map-set Unclaimed-Prizes winner
        { amount: winner-prize, round: round }
      )
      
      (map-set Round-Info round
        {
          tickets-sold: tickets-sold,
          unique-players: unique-players,
          pot-balance: winner-prize,
          winner: (some winner),
          is-drawn: true,
          draw-block: stacks-block-height,
          prize-amount: winner-prize,
          is-claimed: false
        }
      )
      
      (var-set current-round (+ round u1))
      (var-set round-start-block stacks-block-height)
      
      (print { 
        event: "winner-drawn",
        winner: winner, 
        prize: winner-prize, 
        winning-ticket: winning-ticket-id,
        round: round 
      })
      
      (ok { 
        winner: winner, 
        prize: winner-prize, 
        winning-ticket: winning-ticket-id,
        round: round 
      })
    )
  )
)

;; Claim prize - winners call this to receive their STX
(define-public (claim-prize)
  (let
    (
      (prize-info (unwrap! (map-get? Unclaimed-Prizes tx-sender) ERR-NO-PRIZE-TO-CLAIM))
      (prize-amount (get amount prize-info))
      (prize-round (get round prize-info))
    )
    (try! (as-contract? ((with-stx prize-amount)) (unwrap-panic (stx-transfer? prize-amount tx-sender contract-caller))))
    
    (map-delete Unclaimed-Prizes tx-sender)
    
    (match (map-get? Round-Info prize-round)
      round-data 
      (map-set Round-Info prize-round
        (merge round-data { is-claimed: true, pot-balance: u0 })
      )
      true
    )
    
    (print {
      event: "prize-claimed",
      winner: tx-sender,
      amount: prize-amount,
      round: prize-round
    })
    
    (ok { amount: prize-amount, round: prize-round })
  )
)

;; ============================================
;; Read-Only Functions
;; ============================================

(define-read-only (get-current-round)
  (var-get current-round)
)

(define-read-only (get-ticket-price)
  TICKET-PRICE
)

(define-read-only (get-pot-balance)
  (get pot-balance (get-current-round-info))
)

(define-read-only (get-tickets-sold)
  (get tickets-sold (get-current-round-info))
)

(define-read-only (get-unique-players)
  (get unique-players (get-current-round-info))
)

(define-read-only (get-user-ticket-count (user principal))
  (default-to u0 
    (get count 
      (map-get? User-Tickets { round: (var-get current-round), user: user })
    )
  )
)

(define-read-only (get-round-info (round uint))
  (map-get? Round-Info round)
)

(define-read-only (get-last-winner)
  (let
    (
      (current (var-get current-round))
    )
    (if (> current u1)
      (match (map-get? Round-Info (- current u1))
        round-data (get winner round-data)
        none
      )
      none
    )
  )
)

(define-read-only (get-ticket-owner (round uint) (ticket-id uint))
  (match (map-get? Tickets { round: round, ticket-id: ticket-id })
    ticket-data (some (get owner ticket-data))
    none
  )
)

(define-read-only (get-contract-owner)
  CONTRACT-OWNER
)

(define-read-only (get-blocks-until-draw)
  (let
    (
      (blocks-elapsed (- stacks-block-height (var-get round-start-block)))
    )
    (if (>= blocks-elapsed MIN-BLOCKS-BEFORE-DRAW)
      u0
      (- MIN-BLOCKS-BEFORE-DRAW blocks-elapsed)
    )
  )
)

(define-read-only (can-draw)
  (let
    (
      (round-info (get-current-round-info))
      (blocks-elapsed (- stacks-block-height (var-get round-start-block)))
    )
    (and
      (> (get tickets-sold round-info) u0)
      (not (get is-drawn round-info))
      (>= blocks-elapsed MIN-BLOCKS-BEFORE-DRAW)
    )
  )
)

(define-read-only (get-estimated-prize)
  (let
    (
      (pot-balance (get pot-balance (get-current-round-info)))
      (dev-fee (calculate-dev-fee pot-balance))
    )
    (- pot-balance dev-fee)
  )
)

(define-read-only (get-unclaimed-prize (user principal))
  (map-get? Unclaimed-Prizes user)
)

(define-read-only (has-unclaimed-prize (user principal))
  (is-some (map-get? Unclaimed-Prizes user))
)

Functions (24)

FunctionAccessArgs
get-current-round-infoprivate
calculate-dev-feeprivateamount: uint
get-random-seedprivatetarget-block: uint
buff-to-uintprivatehash: (buff 32
buff-to-u8privatebyte: (buff 1
buy-ticketpublic
buy-ticketspublicquantity: uint
draw-winnerpublic
claim-prizepublic
get-current-roundread-only
get-ticket-priceread-only
get-pot-balanceread-only
get-tickets-soldread-only
get-unique-playersread-only
get-user-ticket-countread-onlyuser: principal
get-round-inforead-onlyround: uint
get-last-winnerread-only
get-ticket-ownerread-onlyround: uint, ticket-id: uint
get-contract-ownerread-only
get-blocks-until-drawread-only
can-drawread-only
get-estimated-prizeread-only
get-unclaimed-prizeread-onlyuser: principal
has-unclaimed-prizeread-onlyuser: principal