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)

;; Maximum tickets per transaction (fold list limitation)
(define-constant MAX-TICKETS-PER-TX u50)

;; ============================================
;; 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))
(define-constant ERR-EXCEEDS-MAX-TICKETS (err u109))

;; ============================================
;; 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))
    )
    ;; Validate quantity doesn't exceed max
    (asserts! (<= quantity MAX-TICKETS-PER-TX) ERR-EXCEEDS-MAX-TICKETS)
    
    (try! (stx-transfer? total-cost tx-sender current-contract))
    
    ;; Register each ticket using fold (supports up to 50 tickets)
    (fold register-ticket-fold
      (list u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31 u32 u33 u34 u35 u36 u37 u38 u39 u40 u41 u42 u43 u44 u45 u46 u47 u48 u49 u50)
      { 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))
      )
      
      ;; Transfer dev fee from contract to owner
      (try! (as-contract? ((with-stx dev-fee)) (unwrap-panic (stx-transfer? dev-fee current-contract 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))
    )
    ;; Transfer STX from contract to the winner (contract-caller)
    ;; as-contract? allows contract to transfer its own STX; unwrap-panic the inner response
    (try! (as-contract? ((with-stx prize-amount)) (unwrap-panic (stx-transfer? prize-amount current-contract 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