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 rewards/.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 rewards/.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 rewards/.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 rewards/.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"
}
]
}
23 changes: 23 additions & 0 deletions rewards/Clarinet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[project]
name = 'rewards'
description = ''
authors = []
telemetry = true
cache_dir = './.cache'
requirements = []
[contracts.community]
path = 'contracts/community.clar'
clarity_version = 3
epoch = 'latest'
[repl.analysis]
passes = ['check_checker']

[repl.analysis.check_checker]
strict = false
trusted_sender = false
trusted_caller = false
callee_filter = false

[repl.remote_data]
enabled = false
api_url = 'https://api.hiro.so'
289 changes: 289 additions & 0 deletions rewards/contracts/community.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
;; Community Rewards Distribution Contract
;; Manages reward pools, member stakes, and proportional distributions

;; Constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-found (err u101))
(define-constant err-already-exists (err u102))
(define-constant err-insufficient-balance (err u103))
(define-constant err-zero-amount (err u104))
(define-constant err-invalid-percentage (err u105))
(define-constant err-pool-inactive (err u106))
(define-constant err-unauthorized (err u107))
(define-constant err-no-stake (err u108))
(define-constant err-distribution-failed (err u109))

;; Data Variables
(define-data-var total-pools uint u0)
(define-data-var platform-fee-percentage uint u2) ;; 2% platform fee

;; Data Maps
(define-map reward-pools
{ pool-id: uint }
{
name: (string-ascii 50),
total-rewards: uint,
total-stakes: uint,
is-active: bool,
creator: principal,
created-at: uint,
distribution-count: uint
}
)

(define-map member-stakes
{ pool-id: uint, member: principal }
{
stake-amount: uint,
last-claim-height: uint,
total-claimed: uint,
joined-at: uint
}
)

(define-map pool-admins
{ pool-id: uint, admin: principal }
{ is-admin: bool }
)

;; Read-only functions
(define-read-only (get-pool-info (pool-id uint))
(map-get? reward-pools { pool-id: pool-id })
)

(define-read-only (get-member-stake (pool-id uint) (member principal))
(map-get? member-stakes { pool-id: pool-id, member: member })
)

(define-read-only (get-total-pools)
(var-get total-pools)
)

(define-read-only (get-platform-fee)
(var-get platform-fee-percentage)
)

(define-read-only (is-pool-admin (pool-id uint) (admin principal))
(default-to false (get is-admin (map-get? pool-admins { pool-id: pool-id, admin: admin })))
)

(define-read-only (calculate-member-share (pool-id uint) (member principal))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
(stake (unwrap! (get-member-stake pool-id member) err-no-stake))
(total-stk (get total-stakes pool))
(stake-amt (get stake-amount stake))
)
(if (is-eq total-stk u0)
(ok u0)
(ok (/ (* stake-amt u10000) total-stk))
)
)
)

(define-read-only (calculate-pending-rewards (pool-id uint) (member principal))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
(stake (unwrap! (get-member-stake pool-id member) err-no-stake))
(share-result (unwrap! (calculate-member-share pool-id member) err-not-found))
(available-rewards (get total-rewards pool))
)
(ok (/ (* available-rewards share-result) u10000))
)
)

;; Private functions
(define-private (apply-platform-fee (amount uint))
(let (
(fee (/ (* amount (var-get platform-fee-percentage)) u100))
(net-amount (- amount fee))
)
{ fee: fee, net-amount: net-amount }
)
)

;; Public functions
(define-public (create-pool (name (string-ascii 50)) (initial-rewards uint))
(let (
(new-pool-id (+ (var-get total-pools) u1))
(fee-calc (apply-platform-fee initial-rewards))
)
(asserts! (> initial-rewards u0) err-zero-amount)
(try! (stx-transfer? initial-rewards tx-sender (as-contract tx-sender)))
(map-set reward-pools
{ pool-id: new-pool-id }
{
name: name,
total-rewards: (get net-amount fee-calc),
total-stakes: u0,
is-active: true,
creator: tx-sender,
created-at: stacks-block-height,
distribution-count: u0
}
)
(map-set pool-admins
{ pool-id: new-pool-id, admin: tx-sender }
{ is-admin: true }
)
(var-set total-pools new-pool-id)
(ok new-pool-id)
)
)

(define-public (add-pool-admin (pool-id uint) (new-admin principal))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
)
(asserts! (or (is-eq tx-sender contract-owner) (is-eq tx-sender (get creator pool))) err-owner-only)
(map-set pool-admins
{ pool-id: pool-id, admin: new-admin }
{ is-admin: true }
)
(ok true)
)
)

(define-public (stake-in-pool (pool-id uint) (amount uint))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
(existing-stake (map-get? member-stakes { pool-id: pool-id, member: tx-sender }))
)
(asserts! (get is-active pool) err-pool-inactive)
(asserts! (> amount u0) err-zero-amount)
(match existing-stake
stake-data
(map-set member-stakes
{ pool-id: pool-id, member: tx-sender }
{
stake-amount: (+ (get stake-amount stake-data) amount),
last-claim-height: stacks-block-height,
total-claimed: (get total-claimed stake-data),
joined-at: (get joined-at stake-data)
}
)
(map-set member-stakes
{ pool-id: pool-id, member: tx-sender }
{
stake-amount: amount,
last-claim-height: stacks-block-height,
total-claimed: u0,
joined-at: stacks-block-height
}
)
)
(map-set reward-pools
{ pool-id: pool-id }
(merge pool { total-stakes: (+ (get total-stakes pool) amount) })
)
(ok true)
)
)

(define-public (add-rewards-to-pool (pool-id uint) (amount uint))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
(fee-calc (apply-platform-fee amount))
)
(asserts! (> amount u0) err-zero-amount)
(asserts! (get is-active pool) err-pool-inactive)
(try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
(map-set reward-pools
{ pool-id: pool-id }
(merge pool { total-rewards: (+ (get total-rewards pool) (get net-amount fee-calc)) })
)
(ok true)
)
)

(define-public (claim-rewards (pool-id uint))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
(stake (unwrap! (get-member-stake pool-id tx-sender) err-no-stake))
(pending (unwrap! (calculate-pending-rewards pool-id tx-sender) err-not-found))
)
(asserts! (> pending u0) err-zero-amount)
(asserts! (>= (get total-rewards pool) pending) err-insufficient-balance)
(try! (as-contract (stx-transfer? pending (as-contract tx-sender) tx-sender)))
(map-set member-stakes
{ pool-id: pool-id, member: tx-sender }
(merge stake {
last-claim-height: stacks-block-height,
total-claimed: (+ (get total-claimed stake) pending)
})
)
(map-set reward-pools
{ pool-id: pool-id }
(merge pool { total-rewards: (- (get total-rewards pool) pending) })
)
(ok pending)
)
)

(define-public (withdraw-stake (pool-id uint) (amount uint))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
(stake (unwrap! (get-member-stake pool-id tx-sender) err-no-stake))
(stake-amt (get stake-amount stake))
)
(asserts! (> amount u0) err-zero-amount)
(asserts! (>= stake-amt amount) err-insufficient-balance)
(map-set member-stakes
{ pool-id: pool-id, member: tx-sender }
(merge stake { stake-amount: (- stake-amt amount) })
)
(map-set reward-pools
{ pool-id: pool-id }
(merge pool { total-stakes: (- (get total-stakes pool) amount) })
)
(ok true)
)
)

(define-public (toggle-pool-status (pool-id uint))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
)
(asserts! (or (is-eq tx-sender contract-owner) (is-pool-admin pool-id tx-sender)) err-unauthorized)
(map-set reward-pools
{ pool-id: pool-id }
(merge pool { is-active: (not (get is-active pool)) })
)
(ok true)
)
)

(define-public (update-platform-fee (new-fee uint))
(begin
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
(asserts! (<= new-fee u10) err-invalid-percentage)
(var-set platform-fee-percentage new-fee)
(ok true)
)
)

(define-public (distribute-rewards-bulk (pool-id uint) (members (list 50 principal)))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
)
(asserts! (or (is-eq tx-sender contract-owner) (is-pool-admin pool-id tx-sender)) err-unauthorized)
(asserts! (get is-active pool) err-pool-inactive)
(map-set reward-pools
{ pool-id: pool-id }
(merge pool { distribution-count: (+ (get distribution-count pool) u1) })
)
(ok (len members))
)
)

(define-public (emergency-withdraw (pool-id uint))
(let (
(pool (unwrap! (get-pool-info pool-id) err-not-found))
(stake (unwrap! (get-member-stake pool-id tx-sender) err-no-stake))
)
(asserts! (not (get is-active pool)) err-pool-inactive)
(try! (withdraw-stake pool-id (get stake-amount stake)))
(ok true)
)
)
23 changes: 23 additions & 0 deletions rewards/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

{
"name": "rewards-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": "^3.6.0",
"@types/node": "^24.4.0",
"@stacks/transactions": "^7.2.0",
"chokidar-cli": "^3.0.0",
"vitest": "^3.2.4",
"vitest-environment-clarinet": "^2.3.0"
}
}
Loading