Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions blockchain-raffle/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tests/** linguist-vendored
vitest.config.js linguist-vendored
* text=lf
13 changes: 13 additions & 0 deletions blockchain-raffle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

**/settings/Mainnet.toml
**/settings/Testnet.toml
.cache/**
history.txt

logs
*.log
npm-debug.log*
coverage
*.info
costs-reports.json
node_modules
4 changes: 4 additions & 0 deletions blockchain-raffle/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

{
"files.eol": "\n"
}
19 changes: 19 additions & 0 deletions blockchain-raffle/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

{
"version": "2.0.0",
"tasks": [
{
"label": "check contracts",
"group": "test",
"type": "shell",
"command": "clarinet check"
},
{
"type": "npm",
"script": "test",
"group": "test",
"problemMatcher": [],
"label": "npm test"
}
]
}
19 changes: 19 additions & 0 deletions blockchain-raffle/Clarinet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[project]
name = 'blockchain-raffle'
description = ''
authors = []
telemetry = true
cache_dir = '.\.cache'
requirements = []
[contracts.stx-lottery]
path = 'contracts/stx-lottery.clar'
clarity_version = 2
epoch = 2.5
[repl.analysis]
passes = ['check_checker']

[repl.analysis.check_checker]
strict = false
trusted_sender = false
trusted_caller = false
callee_filter = false
79 changes: 79 additions & 0 deletions blockchain-raffle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Lottery Pool Smart Contract

# About
This smart contract implements a decentralized lottery system on the Stacks blockchain. It allows for creating and managing lotteries, selling tickets, selecting winners, and distributing prizes.

## Features

- Create new lotteries with customizable parameters
- Purchase lottery tickets
- Withdraw tickets during a specified period
- Automatically end lotteries and select winners
- Claim prizes for winners
- Read-only functions for querying lottery state

## Contract Functions

### Administrative Functions

- `start-new-lottery`: Initializes a new lottery with specified parameters.
- `end-current-lottery`: Ends the current lottery and calculates prizes.
- `select-winners`: Selects winners for the ended lottery.

### User Functions

- `purchase-lottery-ticket`: Allows users to buy lottery tickets.
- `withdraw-tickets`: Enables users to withdraw their tickets during the withdrawal period.
- `claim-prize`: Allows winners to claim their prizes.

### Read-Only Functions

- `get-current-ticket-price`: Returns the current ticket price.
- `get-current-lottery-pot`: Returns the total amount in the lottery pot.
- `get-user-ticket-count`: Returns the number of tickets owned by a user.
- `get-total-tickets-sold`: Returns the total number of tickets sold.
- `check-if-lottery-is-active`: Checks if a lottery is currently active.
- `get-lottery-end-block-height`: Returns the block height when the lottery ends.
- `get-withdrawal-end-block-height`: Returns the block height when ticket withdrawals end.
- `get-organizer-fee-percentage`: Returns the organizer's fee percentage.
- `get-winner-info`: Returns information about a specific winner.
- `are-winners-selected`: Checks if winners have been selected for the current lottery.

## Usage

1. Deploy the contract to the Stacks blockchain.
2. Use the `start-new-lottery` function to initialize a new lottery.
3. Users can purchase tickets using the `purchase-lottery-ticket` function.
4. Users can withdraw tickets during the withdrawal period using `withdraw-tickets`.
5. After the lottery end time, the contract owner can end the lottery using `end-current-lottery`.
6. The contract owner then selects winners using `select-winners`.
7. Winners can claim their prizes using the `claim-prize` function.

## Error Handling

The contract includes various error codes for different scenarios:

- `ERR_NOT_AUTHORIZED`: User is not authorized to perform the action.
- `ERR_LOTTERY_INACTIVE`: The lottery is not active.
- `ERR_INSUFFICIENT_BALANCE`: User has insufficient balance.
- `ERR_INVALID_TICKET_PRICE`: The ticket price is invalid.
- `ERR_NO_WINNERS`: No winners could be selected.
- `ERR_NO_TICKETS`: User has no tickets to withdraw.
- `ERR_WITHDRAWAL_PERIOD_ENDED`: The withdrawal period has ended.
- `ERR_LOTTERY_NOT_ENDED`: The lottery has not ended yet.
- `ERR_WINNERS_ALREADY_SELECTED`: Winners have already been selected.

## Security Considerations

- The contract uses various checks to ensure only authorized actions are performed.
- Random winner selection is based on a provided seed and the block height.
- There's a maximum fee percentage (20%) to prevent excessive fees.

## Development and Testing

To develop and test this contract:

1. Set up a Stacks development environment.
2. Use Clarinet for local testing and deployment.
3. Write unit tests to cover all contract functions and edge cases.
4. Deploy to testnet before mainnet for thorough testing.
7 changes: 7 additions & 0 deletions blockchain-raffle/Testnet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[network]
name = "testnet"
stacks_node_rpc_address = "https://api.testnet.hiro.so"
deployment_fee_rate = 10

[accounts.deployer]
mnemonic = "<YOUR PRIVATE TESTNET MNEMONIC HERE>"
185 changes: 185 additions & 0 deletions blockchain-raffle/contracts/stx-lottery.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
;; Lottery Pool Smart Contract

;; Constants
(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_NOT_AUTHORIZED (err u100))
(define-constant ERR_LOTTERY_INACTIVE (err u102))
(define-constant ERR_INSUFFICIENT_BALANCE (err u103))
(define-constant ERR_INVALID_TICKET_PRICE (err u104))
(define-constant ERR_NO_WINNERS (err u105))
(define-constant ERR_NO_TICKETS (err u106))
(define-constant ERR_WITHDRAWAL_PERIOD_ENDED (err u107))
(define-constant ERR_LOTTERY_NOT_ENDED (err u108))
(define-constant ERR_WINNERS_ALREADY_SELECTED (err u109))
(define-constant ERR_INVALID_DURATION (err u110))
(define-constant ERR_INVALID_WITHDRAWAL_PERIOD (err u111))
(define-constant ERR_INVALID_WINNER_ID (err u112))

;; Data Variables
(define-data-var is-lottery-active bool false)
(define-data-var current-ticket-price uint u1000000) ;; 1 STX
(define-data-var current-lottery-pot uint u0)
(define-data-var total-tickets-sold uint u0)
(define-data-var number-of-winners uint u1)
(define-data-var lottery-end-block-height uint u0)
(define-data-var withdrawal-end-block-height uint u0)
(define-data-var organizer-fee-percentage uint u5) ;; 5% fee
(define-data-var prize-per-winner uint u0)
(define-data-var winners-selected bool false)

;; Maps
(define-map ticket-ownership {ticket-id: uint} {owner: principal})
(define-map user-ticket-count principal uint)
(define-map winners {winner-id: uint} {address: principal, claimed: bool})

;; Private Functions
(define-private (is-contract-owner)
(is-eq tx-sender CONTRACT_OWNER))

(define-private (validate-lottery-is-active)
(if (var-get is-lottery-active)
(ok true)
ERR_LOTTERY_INACTIVE))

(define-private (validate-sufficient-balance (required-balance uint))
(if (>= (stx-get-balance tx-sender) required-balance)
(ok true)
ERR_INSUFFICIENT_BALANCE))

(define-private (select-random-winner (random-seed uint) (ticket-id uint))
(mod (+ random-seed ticket-id) (var-get total-tickets-sold)))

(define-private (transfer-prize-to-winner (winner-address principal) (prize-amount uint))
(as-contract (stx-transfer? prize-amount tx-sender winner-address)))

(define-private (calculate-organizer-fee (total-amount uint))
(/ (* total-amount (var-get organizer-fee-percentage)) u100))

;; Public Functions
(define-public (start-new-lottery (duration-in-blocks uint) (withdrawal-period uint) (ticket-price uint) (winner-count uint) (fee-percentage uint))
(begin
(asserts! (is-contract-owner) ERR_NOT_AUTHORIZED)
(asserts! (> ticket-price u0) ERR_INVALID_TICKET_PRICE)
(asserts! (> winner-count u0) ERR_NO_WINNERS)
(asserts! (<= fee-percentage u20) ERR_NOT_AUTHORIZED) ;; Max 20% fee
(asserts! (not (var-get is-lottery-active)) ERR_NOT_AUTHORIZED)
(asserts! (> duration-in-blocks u0) ERR_INVALID_DURATION)
(asserts! (> withdrawal-period u0) ERR_INVALID_WITHDRAWAL_PERIOD)
(var-set is-lottery-active true)
(var-set current-ticket-price ticket-price)
(var-set current-lottery-pot u0)
(var-set total-tickets-sold u0)
(var-set number-of-winners winner-count)
(var-set lottery-end-block-height (+ block-height duration-in-blocks))
(var-set withdrawal-end-block-height (+ block-height withdrawal-period))
(var-set organizer-fee-percentage fee-percentage)
(var-set winners-selected false)
(ok true)))

(define-public (purchase-lottery-ticket)
(let ((ticket-price (var-get current-ticket-price)))
(begin
(try! (validate-lottery-is-active))
(try! (validate-sufficient-balance ticket-price))
(try! (stx-transfer? ticket-price tx-sender (as-contract tx-sender)))
(var-set current-lottery-pot (+ (var-get current-lottery-pot) ticket-price))
(var-set total-tickets-sold (+ (var-get total-tickets-sold) u1))
(map-set ticket-ownership {ticket-id: (var-get total-tickets-sold)} {owner: tx-sender})
(map-set user-ticket-count tx-sender (+ (default-to u0 (map-get? user-ticket-count tx-sender)) u1))
(ok (var-get total-tickets-sold)))))

(define-public (withdraw-tickets (ticket-count uint))
(let ((user-tickets (default-to u0 (map-get? user-ticket-count tx-sender)))
(refund-amount (* ticket-count (var-get current-ticket-price))))
(begin
(try! (validate-lottery-is-active))
(asserts! (<= block-height (var-get withdrawal-end-block-height)) ERR_WITHDRAWAL_PERIOD_ENDED)
(asserts! (>= user-tickets ticket-count) ERR_NO_TICKETS)
(var-set current-lottery-pot (- (var-get current-lottery-pot) refund-amount))
(var-set total-tickets-sold (- (var-get total-tickets-sold) ticket-count))
(map-set user-ticket-count tx-sender (- user-tickets ticket-count))
(as-contract (stx-transfer? refund-amount tx-sender tx-sender)))))

(define-public (end-current-lottery)
(let ((total-pot (var-get current-lottery-pot))
(winner-count (var-get number-of-winners))
(total-ticket-count (var-get total-tickets-sold))
(organizer-fee (calculate-organizer-fee total-pot)))
(begin
(asserts! (is-contract-owner) ERR_NOT_AUTHORIZED)
(asserts! (>= block-height (var-get lottery-end-block-height)) ERR_LOTTERY_NOT_ENDED)
(try! (validate-lottery-is-active))
(asserts! (> total-ticket-count u0) ERR_NO_WINNERS)
(var-set is-lottery-active false)
(try! (as-contract (stx-transfer? organizer-fee tx-sender CONTRACT_OWNER)))
(let ((prize-pool (- total-pot organizer-fee)))
(var-set prize-per-winner (/ prize-pool winner-count)))
(ok true))))

(define-public (select-winners (random-seed uint))
(let ((winner-count (var-get number-of-winners))
(total-ticket-count (var-get total-tickets-sold)))
(begin
(asserts! (is-contract-owner) ERR_NOT_AUTHORIZED)
(asserts! (not (var-get is-lottery-active)) ERR_LOTTERY_INACTIVE)
(asserts! (not (var-get winners-selected)) ERR_WINNERS_ALREADY_SELECTED)
(asserts! (> total-ticket-count u0) ERR_NO_WINNERS)
(var-set winners-selected true)
(let ((selected-winners (fold select-winner-and-save
(list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9)
{random-seed: random-seed, current-winner-id: u0, remaining-winners: winner-count})))
(ok (get current-winner-id selected-winners))))))

(define-private (select-winner-and-save (index uint) (context {random-seed: uint, current-winner-id: uint, remaining-winners: uint}))
(if (> (get remaining-winners context) u0)
(let ((winning-ticket-id (select-random-winner (get random-seed context) index))
(winner-address (get owner (unwrap-panic (map-get? ticket-ownership {ticket-id: (+ winning-ticket-id u1)})))))
(begin
(map-set winners {winner-id: (get current-winner-id context)} {address: winner-address, claimed: false})
{random-seed: (+ (get random-seed context) u1),
current-winner-id: (+ (get current-winner-id context) u1),
remaining-winners: (- (get remaining-winners context) u1)}))
context))

(define-public (claim-prize (winner-id uint))
(let ((winner-info (unwrap! (map-get? winners {winner-id: winner-id}) ERR_INVALID_WINNER_ID))
(winner-address (get address winner-info))
(claimed (get claimed winner-info)))
(begin
(asserts! (is-eq tx-sender winner-address) ERR_NOT_AUTHORIZED)
(asserts! (not claimed) ERR_NOT_AUTHORIZED)
(try! (transfer-prize-to-winner winner-address (var-get prize-per-winner)))
(asserts! (< winner-id (var-get number-of-winners)) ERR_INVALID_WINNER_ID)
(map-set winners {winner-id: winner-id} {address: winner-address, claimed: true})
(ok true))))

;; Read-Only Functions
(define-read-only (get-current-ticket-price)
(ok (var-get current-ticket-price)))

(define-read-only (get-current-lottery-pot)
(ok (var-get current-lottery-pot)))

(define-read-only (get-user-ticket-count (user-address principal))
(ok (default-to u0 (map-get? user-ticket-count user-address))))

(define-read-only (get-total-tickets-sold)
(ok (var-get total-tickets-sold)))

(define-read-only (check-if-lottery-is-active)
(ok (var-get is-lottery-active)))

(define-read-only (get-lottery-end-block-height)
(ok (var-get lottery-end-block-height)))

(define-read-only (get-withdrawal-end-block-height)
(ok (var-get withdrawal-end-block-height)))

(define-read-only (get-organizer-fee-percentage)
(ok (var-get organizer-fee-percentage)))

(define-read-only (get-winner-info (winner-id uint))
(ok (map-get? winners {winner-id: winner-id})))

(define-read-only (are-winners-selected)
(ok (var-get winners-selected)))
24 changes: 24 additions & 0 deletions blockchain-raffle/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

{
"name": "blockchain-raffle-tests",
"version": "1.0.0",
"description": "Run unit tests on this project.",
"type": "module",
"private": true,
"scripts": {
"test": "vitest run",
"test:report": "vitest run -- --coverage --costs",
"test:watch": "chokidar \"tests/**/*.ts\" \"contracts/**/*.clar\" -c \"npm run test:report\""
},
"author": "",
"license": "ISC",
"dependencies": {
"@hirosystems/clarinet-sdk": "^2.3.2",
"@stacks/transactions": "^6.12.0",
"chokidar-cli": "^3.0.0",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vitest": "^1.3.1",
"vitest-environment-clarinet": "^2.0.0"
}
}
Loading