Source Code

;; BlockLotto - A decentralized lottery system for Stacks (Clarity)
;; Entry fee is fixed (10 STX). Contract uses pull-payments for safety.
;; Contract creator has no ability to withdraw funds from the contract.

;; -----------------------------
;; Constants (error codes, params)
;; -----------------------------
(define-constant ERR-ALREADY-INITIALIZED u100)
(define-constant ERR-NOT-OPEN u101)
(define-constant ERR-DEADLINE-PASSED u102)
(define-constant ERR-DEADLINE-NOT-PASSED u103)
(define-constant ERR-ALREADY-ENTERED u104)
(define-constant ERR-INSUFFICIENT-FEE u105)
(define-constant ERR-MAX-PARTICIPANTS u106)
(define-constant ERR-NOT-ENOUGH-PLAYERS u107)
(define-constant ERR-WINNER-NOT-SET u108)
(define-constant ERR-NOT-WINNER u109)
(define-constant ERR-ALREADY-CLAIMED u110)
(define-constant ERR-INVALID-STATUS u111)
(define-constant ERR-REFUNDS-NOT-ALLOWED u112)
(define-constant ERR-NOT-ADMIN u113)
(define-constant ERR-PAUSED u114)

;; Operational parameters
(define-constant entry-fee u1000000) ;; 1 STX
(define-constant min-players u2)
(define-constant max-participants u100)

;; Lottery statuses
(define-constant STATUS-OPEN u0)
(define-constant STATUS-READY-TO-DRAW u1)
(define-constant STATUS-COMPLETED u2)
(define-constant STATUS-REFUNDED u3)

;; -----------------------------
;; State variables
;; -----------------------------
(define-data-var status uint STATUS-OPEN)
(define-data-var target-block-height uint u0)
(define-data-var total-participants uint u0)
(define-data-var winner (optional principal) none)
(define-data-var creator (optional principal) none)
(define-data-var paused bool false)

;; -----------------------------
;; Maps
;; -----------------------------
(define-map participant-index {participant: principal} {index: uint})
(define-map participant-by-index {index: uint} {participant: principal})
(define-map payouts {recipient: principal} {amount: uint})
(define-map refund-claimed {participant: principal} {claimed: bool})

;; -----------------------------
;; Private functions
;; -----------------------------
(define-private (get-current-block-height)
  block-height)

(define-private (add-payout (recipient principal) (amount uint))
  (let ((existing (map-get? payouts {recipient: recipient})))
    (match existing
      existing-row
      (map-set payouts {recipient: recipient} {amount: (+ (get amount existing-row) amount)})
      (map-set payouts {recipient: recipient} {amount: amount}))
    (ok true)))

;; -----------------------------
;; Public functions
;; -----------------------------
(define-public (init (target-block uint))
  (begin
    (asserts! (is-none (var-get creator)) (err ERR-ALREADY-INITIALIZED))
    (var-set creator (some tx-sender))
    (var-set target-block-height target-block)
    (var-set paused false)
    (ok true)))

(define-public (pause)
  (let ((admin (unwrap! (var-get creator) (err ERR-INVALID-STATUS))))
    (asserts! (is-eq admin tx-sender) (err ERR-NOT-ADMIN))
    (var-set paused true)
    (ok true)))

(define-public (unpause)
  (let ((admin (unwrap! (var-get creator) (err ERR-INVALID-STATUS))))
    (asserts! (is-eq admin tx-sender) (err ERR-NOT-ADMIN))
    (var-set paused false)
    (ok true)))

(define-public (enter-lottery)
  (let ((target (var-get target-block-height))
        (current-height (get-current-block-height))
        (count (var-get total-participants)))
    (asserts! (is-eq (var-get status) STATUS-OPEN) (err ERR-NOT-OPEN))
    (asserts! (not (var-get paused)) (err ERR-PAUSED))
    (asserts! (> target u0) (err ERR-INVALID-STATUS))
    (asserts! (< current-height target) (err ERR-DEADLINE-PASSED))
    (asserts! (is-none (map-get? participant-index {participant: tx-sender})) (err ERR-ALREADY-ENTERED))
    (asserts! (< count max-participants) (err ERR-MAX-PARTICIPANTS))
    
    (try! (stx-transfer? entry-fee tx-sender (as-contract tx-sender)))
    
    (map-set participant-index {participant: tx-sender} {index: count})
    (map-set participant-by-index {index: count} {participant: tx-sender})
    (var-set total-participants (+ count u1))
    
    (if (or (>= (+ count u1) min-players) (>= current-height target))
      (var-set status STATUS-READY-TO-DRAW)
      true)
    
    (ok true)))

(define-public (draw-winner)
  (let ((st (var-get status))
        (target (var-get target-block-height))
        (current-height (get-current-block-height))
        (count (var-get total-participants)))
    (asserts! (or (is-eq st STATUS-OPEN) (is-eq st STATUS-READY-TO-DRAW)) (err ERR-INVALID-STATUS))
    (asserts! (not (var-get paused)) (err ERR-PAUSED))
    (asserts! (>= current-height target) (err ERR-DEADLINE-NOT-PASSED))
    (asserts! (>= count min-players) (err ERR-NOT-ENOUGH-PLAYERS))
    
    ;; Use target block for randomness (it's guaranteed to exist and be stable)
    (let ((header-hash (unwrap! (get-block-info? id-header-hash target) (err ERR-INVALID-STATUS))))
      ;; Convert first 16 bytes of hash to uint using hash160 for randomness
      (let ((random-hash (hash160 header-hash)))
        (let ((random-seed (+ (len random-hash) target current-height)))
          (let ((winner-idx (mod random-seed count)))
            (let ((winner-entry (unwrap! (map-get? participant-by-index {index: winner-idx}) (err ERR-WINNER-NOT-SET))))
              (let ((winner-pr (get participant winner-entry)))
                (var-set winner (some winner-pr))
                (var-set status STATUS-COMPLETED)
                (unwrap-panic (add-payout winner-pr (* count entry-fee)))
                (ok winner-pr)))))))))
(define-public (claim-prize)
  (let ((winner-pr (unwrap! (var-get winner) (err ERR-WINNER-NOT-SET))))
    (asserts! (is-eq (var-get status) STATUS-COMPLETED) (err ERR-INVALID-STATUS))
    (asserts! (is-eq winner-pr tx-sender) (err ERR-NOT-WINNER))
    
    (let ((payout-entry (unwrap! (map-get? payouts {recipient: tx-sender}) (err ERR-WINNER-NOT-SET))))
      (let ((amt (get amount payout-entry)))
        (map-delete payouts {recipient: tx-sender})
        (try! (as-contract (stx-transfer? amt tx-sender winner-pr)))
        (ok amt)))))

(define-public (refund)
  (let ((st (var-get status))
        (target (var-get target-block-height))
        (current-height (get-current-block-height))
        (count (var-get total-participants)))
    
    (if (is-eq st STATUS-REFUNDED)
      true
      (begin
        (asserts! (>= current-height target) (err ERR-DEADLINE-NOT-PASSED))
        (asserts! (< count min-players) (err ERR-REFUNDS-NOT-ALLOWED))
        (var-set status STATUS-REFUNDED)
        true))
    
    (let ((participant-entry (unwrap! (map-get? participant-index {participant: tx-sender}) (err ERR-NOT-ENOUGH-PLAYERS))))
      (asserts! (is-none (map-get? refund-claimed {participant: tx-sender})) (err ERR-ALREADY-CLAIMED))
      
      (map-set refund-claimed {participant: tx-sender} {claimed: true})
      (try! (as-contract (stx-transfer? entry-fee tx-sender tx-sender)))
      (ok entry-fee))))

;; -----------------------------
;; Read-only functions
;; -----------------------------
(define-read-only (get-lottery-info)
  (ok {
    status: (var-get status),
    target-block-height: (var-get target-block-height),
    total-participants: (var-get total-participants),
    prize-pool: (* (var-get total-participants) entry-fee),
    entry-fee: entry-fee,
    min-players: min-players,
    max-participants: max-participants,
    winner: (var-get winner),
    creator: (var-get creator),
    paused: (var-get paused)
  }))

(define-read-only (get-participants)
  (ok (list)))

(define-read-only (get-participant (p principal))
  (ok (map-get? participant-index {participant: p})))

(define-read-only (get-winner)
  (ok (var-get winner)))

(define-read-only (is-refund-claimed (p principal))
  (match (map-get? refund-claimed {participant: p})
    entry (ok (get claimed entry))
    (ok false)))

Functions (14)

FunctionAccessArgs
get-current-block-heightprivate
add-payoutprivaterecipient: principal, amount: uint
initpublictarget-block: uint
pausepublic
unpausepublic
enter-lotterypublic
draw-winnerpublic
claim-prizepublic
refundpublic
get-lottery-inforead-only
get-participantsread-only
get-participantread-onlyp: principal
get-winnerread-only
is-refund-claimedread-onlyp: principal