Source Code

;; title: lottery-demo-v3
;; version: 1.0.0
;; summary: Simple raffle with ticket purchases and block-hash draw.
;; clarity: 4

;; constants
(define-constant contract-version "1.0.0")
(define-constant min-ticket u1000000)
(define-constant max-ticket u100000000)
(define-constant status-open u0)
(define-constant status-drawn u1)
(define-constant status-canceled u2)
(define-constant err-not-admin (err u900))
(define-constant err-admin-unset (err u901))
(define-constant err-admin-set (err u902))
(define-constant err-paused (err u903))
(define-constant err-admin-locked (err u904))
(define-constant err-ticket-low (err u400))
(define-constant err-ticket-high (err u401))
(define-constant err-not-open (err u402))
(define-constant err-transfer (err u403))
(define-constant err-too-early (err u404))
(define-constant err-no-tickets (err u405))
(define-constant err-not-found (err u406))

;; data vars
(define-data-var next-round-id uint u0)
(define-data-var admin (optional principal) none)
(define-data-var paused bool false)
(define-data-var admin-locked bool false)

;; data maps
(define-map rounds
  {id: uint}
  {
    id: uint,
    ticket-price: uint,
    total: uint,
    close-height: uint,
    status: uint
  }
)

(define-map tickets
  {round-id: uint, index: uint}
  {player: principal}
)

;; private helpers
;; transfer STX out of the contract
(define-private (transfer-from-contract (amount uint) (recipient principal))
  (as-contract (stx-transfer? amount tx-sender recipient)))


(define-private (assert-admin)
  (match (var-get admin)
    admin-principal (if (is-eq admin-principal tx-sender) (ok true) err-not-admin)
    err-admin-unset))

(define-private (assert-not-paused)
  (if (var-get paused) err-paused (ok true)))

(define-private (random-from-height (height uint))
  (let (
    (hash-opt (get-stacks-block-info? header-hash height))
  )
    (match hash-opt
      hash
      (let ((part (unwrap-panic (slice? hash u0 u16))))
        (buff-to-uint-be (unwrap-panic (as-max-len? part u16))))
      u0)))

;; admin
(define-public (init-admin)
  (begin
    (asserts! (is-none (var-get admin)) err-admin-set)
    (var-set admin (some tx-sender))
    (ok true)))

(define-public (set-admin (new-admin principal))
  (begin
    (unwrap! (assert-admin) err-not-admin)
    (asserts! (not (var-get admin-locked)) err-admin-locked)
    (var-set admin (some new-admin))
    (ok true)))

(define-public (lock-admin)
  (begin
    (unwrap! (assert-admin) err-not-admin)
    (var-set admin-locked true)
    (ok true)))

(define-public (pause)
  (begin
    (unwrap! (assert-admin) err-not-admin)
    (var-set paused true)
    (ok true)))

(define-public (unpause)
  (begin
    (unwrap! (assert-admin) err-not-admin)
    (var-set paused false)
    (ok true)))

;; public functions
(define-public (create-round (ticket-price uint) (duration uint))
  (begin
    (unwrap! (assert-admin) err-not-admin)
    (unwrap! (assert-not-paused) err-paused)
    (asserts! (>= ticket-price min-ticket) err-ticket-low)
    (asserts! (<= ticket-price max-ticket) err-ticket-high)
    (let
      (
        (round-id (var-get next-round-id))
      )
      (begin
        (map-set rounds {id: round-id}
          {
            id: round-id,
            ticket-price: ticket-price,
            total: u0,
            close-height: (+ stacks-block-height duration),
            status: status-open
          })
        (var-set next-round-id (+ round-id u1))
        (ok round-id)))))

(define-public (buy-ticket (round-id uint))
  (let ((round (unwrap! (map-get? rounds {id: round-id}) err-not-found)))
    (begin
      (unwrap! (assert-not-paused) err-paused)
      (asserts! (is-eq (get status round) status-open) err-not-open)
      (let
        (
          (self (as-contract tx-sender))
          (ticket (get ticket-price round))
          (index (get total round))
        )
        (begin
          (unwrap! (stx-transfer? ticket tx-sender self) err-transfer)
          (map-set tickets {round-id: round-id, index: index} {player: tx-sender})
          (map-set rounds {id: round-id} (merge round {total: (+ index u1)}))
          (ok true))))))

(define-public (draw (round-id uint))
  (let ((round (unwrap! (map-get? rounds {id: round-id}) err-not-found)))
    (begin
      (unwrap! (assert-admin) err-not-admin)
      (asserts! (is-eq (get status round) status-open) err-not-open)
      (asserts! (> stacks-block-height (get close-height round)) err-too-early)
      (asserts! (> (get total round) u0) err-no-tickets)
      (let
        (
          (total (get total round))
          (ticket (get ticket-price round))
          (winner-index (mod (random-from-height (+ (get close-height round) u1)) total))
          (winner (get player (unwrap-panic (map-get? tickets {round-id: round-id, index: winner-index}))))
          (payout (* ticket total))
        )
        (begin
          (unwrap! (transfer-from-contract payout winner) err-transfer)
          (map-set rounds {id: round-id} (merge round {status: status-drawn}))
          (ok {winner: winner, payout: payout}))))))

(define-public (cancel-round (round-id uint))
  (let ((round (unwrap! (map-get? rounds {id: round-id}) err-not-found)))
    (begin
      (unwrap! (assert-admin) err-not-admin)
      (asserts! (is-eq (get status round) status-open) err-not-open)
      (asserts! (is-eq (get total round) u0) err-no-tickets)
      (map-set rounds {id: round-id} (merge round {status: status-canceled}))
      (ok true))))

;; read only functions
(define-read-only (get-next-round-id)
  (var-get next-round-id))

(define-read-only (get-round (round-id uint))
  (map-get? rounds {id: round-id}))

(define-read-only (get-admin)
  (var-get admin))

(define-read-only (is-paused)
  (var-get paused))

(define-read-only (is-admin-locked)
  (var-get admin-locked))

(define-read-only (get-version)
  contract-version)

Functions (19)

FunctionAccessArgs
transfer-from-contractprivateamount: uint, recipient: principal
assert-adminprivate
assert-not-pausedprivate
random-from-heightprivateheight: uint
init-adminpublic
set-adminpublicnew-admin: principal
lock-adminpublic
pausepublic
unpausepublic
create-roundpublicticket-price: uint, duration: uint
buy-ticketpublicround-id: uint
drawpublicround-id: uint
cancel-roundpublicround-id: uint
get-next-round-idread-only
get-roundread-onlyround-id: uint
get-adminread-only
is-pausedread-only
is-admin-lockedread-only
get-versionread-only