Source Code

;; SPDX-License-Identifier: BUSL-1.1
(use-trait ft-trait 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.trait-sip-010.sip-010-trait)

(define-constant err-not-authorized (err u1000))
(define-constant err-get-block-info (err u1001))
(define-constant err-invalid-campaign-registration (err u1002))
(define-constant err-invalid-campaign-id (err u1003))
(define-constant err-registration-cutoff-passed (err u1004))
(define-constant err-stake-cutoff-passed (err u1005))
(define-constant err-campaign-not-ended (err u1006))
(define-constant err-token-mismatch (err u1007))
(define-constant err-invalid-input (err u1008))
(define-constant err-invalid-reward-token (err u1010))
(define-constant err-already-claimed (err u1011))
(define-constant err-stake-end-passed (err u1005))

(define-constant ONE_8 u100000000)

(define-data-var campaign-nonce uint u0)
(define-map campaigns uint { registration-cutoff: uint, voting-cutoff: uint, stake-cutoff: uint, stake-end: uint, reward-amount: uint, snapshot-block: uint })
(define-map campaign-registrations { campaign-id: uint, pool-id: uint } { reward-token: principal, reward-amount: uint, total-staked: uint })
(define-map campaign-stakers { campaign-id: uint, pool-id: uint, staker: principal } { amount: uint, claimed: bool })
(define-data-var whitelisted-pools (list 200 uint) (list))

(define-map campaign-voted { campaign-id: uint, voter: principal } bool)
(define-map campaign-pool-votes { campaign-id: uint, pool-id: uint } uint)
(define-map campaign-total-vote uint uint)
(define-map campaign-registered-pools uint (list 200 uint))

;; read-only calls

(define-read-only (is-dao-or-extension)
    (ok (asserts! (or (is-eq tx-sender 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.executor-dao) (contract-call? 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.executor-dao is-extension contract-caller)) err-not-authorized)))

(define-read-only (block-timestamp)
  (ok (unwrap! (get-stacks-block-info? time (- stacks-block-height u1)) err-get-block-info)))

(define-read-only (get-campaign-nonce)
  (var-get campaign-nonce))

(define-read-only (get-campaign-or-fail (campaign-id uint))
	(ok (unwrap! (map-get? campaigns campaign-id) err-invalid-campaign-id)))

(define-read-only (get-campaigns-or-fail-many (campaign-ids (list 200 uint)))
	(map get-campaign-or-fail campaign-ids))

(define-read-only (get-campaign-registration-by-id-or-fail (campaign-id uint) (pool-id uint))
	(ok (unwrap! (map-get? campaign-registrations { campaign-id: campaign-id, pool-id: pool-id }) err-invalid-campaign-registration)))

(define-read-only (get-campaign-registration-by-id-or-fail-many (campaign-ids (list 200 uint)) (pool-ids (list 200 uint)))
	(map get-campaign-registration-by-id-or-fail campaign-ids pool-ids))

(define-read-only (get-campaign-staker-or-default (campaign-id uint) (pool-id uint) (staker principal))
    (default-to { amount: u0, claimed: false } (map-get? campaign-stakers { campaign-id: campaign-id, pool-id: pool-id, staker: staker })))

(define-read-only (get-campaign-staker-or-default-many (campaign-ids (list 200 uint)) (pool-ids (list 200 uint)) (stakers (list 200 principal)))
    (map get-campaign-staker-or-default campaign-ids pool-ids stakers))

(define-read-only (get-pool-whitelisted (pool-id uint))
    (is-some (index-of (var-get whitelisted-pools) pool-id)))

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

;; New read-only function for voting power
(define-read-only (voting-power (campaign-id uint) (address principal))
  (let (
    (campaign (unwrap! (map-get? campaigns campaign-id) err-invalid-campaign-id))
    (snapshot-block (get snapshot-block campaign))
    (alex-balance (unwrap-panic (at-block (unwrap-panic (get-stacks-block-info? id-header-hash snapshot-block))
      (contract-call? 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex get-balance address))))
    (auto-alex-balance (unwrap-panic (at-block (unwrap-panic (get-stacks-block-info? id-header-hash snapshot-block))
      (contract-call? 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.auto-alex-v3 get-balance address))))
    (total-voting-power (+ alex-balance auto-alex-balance))
    (voted (default-to false (map-get? campaign-voted { campaign-id: campaign-id, voter: address }))))
    (ok { voting-power: total-voting-power, voted: voted })))

(define-read-only (get-campaign-registered-pools (campaign-id uint))
	(ok (default-to (list) (map-get? campaign-registered-pools campaign-id))))

;; New read-only function to get campaign summary
(define-read-only (get-campaign-summary (campaign-id uint))
  (let (
    (campaign (unwrap! (map-get? campaigns campaign-id) (err err-invalid-campaign-id)))
    (registered-pool-ids (default-to (list) (map-get? campaign-registered-pools campaign-id)))
    (pool-summaries-result (fold get-pool-summary-fold registered-pool-ids { campaign-id: campaign-id, summaries: (list) }))
    (total-votes (default-to u0 (map-get? campaign-total-vote campaign-id))))
    (ok (merge campaign {
      pool-summaries: (get summaries pool-summaries-result),
      total-votes: total-votes,
    }))))

;; Helper function to get summary for a single pool
(define-private (get-pool-summary-fold (pool-id uint) (acc { campaign-id: uint, summaries: (list 200 {
    pool-id: uint,
    votes: uint,
    reward-token: principal,
    reward-token-amount: uint,
    total-staked: uint
  })}))
  (let (
    (campaign-id (get campaign-id acc))
    (registration (unwrap-panic (map-get? campaign-registrations { campaign-id: campaign-id, pool-id: pool-id })))
    (votes (default-to u0 (map-get? campaign-pool-votes { campaign-id: campaign-id, pool-id: pool-id })))
    (summary {
      pool-id: pool-id,
      votes: votes,
      reward-token: (get reward-token registration),
      reward-token-amount: (get reward-amount registration),
      total-staked: (get total-staked registration)
    }))
    (merge acc { summaries: (unwrap-panic (as-max-len? (append (get summaries acc) summary) u200)) })))

;; New read-only function to get staker history across multiple campaigns
(define-read-only (get-campaign-staker-history-many (address principal) (campaign-ids (list 200 uint)))
  (get history (fold get-campaign-staker-history campaign-ids { address: address, history: (list) })))

;; Helper function to get staker history for a single campaign
(define-private (get-campaign-staker-history (campaign-id uint) (acc { address: principal, history: (list 1000 { campaign-id: uint, pool-id: uint, staker-info: { amount: uint, claimed: bool } }) }))
  (let (
    (address (get address acc))
    (registered-pools (default-to (list) (map-get? campaign-registered-pools campaign-id)))
    (campaign-history (fold get-pool-staker-history registered-pools { campaign-id: campaign-id, address: address, history: (list) })))
    (merge acc { history: (unwrap-panic (as-max-len? (concat (get history acc) (get history campaign-history)) u1000)) })))

;; Helper function to get staker history for a single pool in a campaign
(define-private (get-pool-staker-history (pool-id uint) (acc { campaign-id: uint, address: principal, history: (list 1000 { campaign-id: uint, pool-id: uint, staker-info: { amount: uint, claimed: bool } }) }))
  (let (
    (campaign-id (get campaign-id acc))
    (address (get address acc))
    (staker-info (get-campaign-staker-or-default campaign-id pool-id address))
    (staker-record { campaign-id: campaign-id, pool-id: pool-id, staker-info: staker-info })
    (updated-history (if (> (get amount (get staker-info staker-record)) u0)
      (unwrap-panic (as-max-len? (append (get history acc) staker-record) u1000))
      (get history acc))))
    (merge acc { history: updated-history })))

;; public calls

(define-public (stake (pool-id uint) (campaign-id uint) (amount uint))
    (let (
			(current-timestamp (try! (block-timestamp)))
			(campaign-details (try! (get-campaign-or-fail campaign-id)))
			(campaign-registration-details (try! (get-campaign-registration-by-id-or-fail campaign-id pool-id)))
			(staker-info (get-campaign-staker-or-default campaign-id pool-id tx-sender))
			(updated-staker-stake (+ (get amount staker-info) amount))
			(updated-total-stake (+ (get total-staked campaign-registration-details) amount)))
		(asserts! (< current-timestamp (get stake-cutoff campaign-details)) err-stake-cutoff-passed)

		(try! (contract-call? 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-amm-pool-v2-01 transfer-fixed pool-id amount tx-sender (as-contract tx-sender)))
		(map-set campaign-registrations { campaign-id: campaign-id, pool-id: pool-id } (merge campaign-registration-details { total-staked: updated-total-stake }))
		(map-set campaign-stakers { campaign-id: campaign-id, pool-id: pool-id, staker: tx-sender } { amount: updated-staker-stake, claimed: false })

		(print { notification: "stake", payload: { sender: tx-sender, campaign-id: campaign-id, pool-id: pool-id, total-stake: updated-total-stake, staker-stake: updated-staker-stake, amount: amount }})
		(ok true)))

(define-public (unstake (pool-id uint) (campaign-id uint) (reward-token-trait <ft-trait>))
   (let (
			(sender tx-sender)
			(current-timestamp (try! (block-timestamp)))
			(campaign-details (try! (get-campaign-or-fail campaign-id)))
			(campaign-registration-details (try! (get-campaign-registration-by-id-or-fail campaign-id pool-id)))
			(staker-info (get-campaign-staker-or-default campaign-id pool-id sender))
			(staker-stake (get amount staker-info))
			(reward (div-down (mul-down (get reward-amount campaign-registration-details) staker-stake) (get total-staked campaign-registration-details)))
			(pool-votes (default-to u0 (map-get? campaign-pool-votes { campaign-id: campaign-id, pool-id: pool-id })))
			(total-votes (default-to u0 (map-get? campaign-total-vote campaign-id)))
			(total-alex-reward-for-pool (if (is-eq total-votes u0)
					u0
					(div-down (mul-down (get reward-amount campaign-details) pool-votes) total-votes)))
			(alex-reward (div-down (mul-down total-alex-reward-for-pool staker-stake) (get total-staked campaign-registration-details))))
		(asserts! (< (get stake-end campaign-details) current-timestamp) err-campaign-not-ended)
		(asserts! (is-eq (contract-of reward-token-trait) (get reward-token campaign-registration-details)) err-token-mismatch)
		(asserts! (not (get claimed staker-info)) err-already-claimed)

		(and (> reward u0) (as-contract (try! (contract-call? reward-token-trait transfer-fixed reward tx-sender sender none))))
		(and (> alex-reward u0) (try! (contract-call? 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-alex mint-fixed alex-reward sender)))
		(map-set campaign-stakers { campaign-id: campaign-id, pool-id: pool-id, staker: sender } { amount: u0, claimed: true })
		(as-contract (try! (contract-call? 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.token-amm-pool-v2-01 transfer-fixed pool-id staker-stake tx-sender sender)))

		(print { notification: "unstake", payload: { sender: tx-sender, campaign-id: campaign-id, pool-id: pool-id, reward: reward, alex-reward: alex-reward, staker-stake: staker-stake }})
		(ok true)))

(define-public (register-for-campaign (pool-id uint) (campaign-id uint) (reward-token-trait <ft-trait>) (reward-amount uint))
	(let (
			(reward-token (contract-of reward-token-trait))
			(current-timestamp (try! (block-timestamp)))
			(campaign-details (try! (get-campaign-or-fail campaign-id)))
			(registered-pools (default-to (list) (map-get? campaign-registered-pools campaign-id))))
		(asserts! (get-pool-whitelisted pool-id) err-not-authorized)
		(asserts! (< current-timestamp (get registration-cutoff campaign-details)) err-registration-cutoff-passed)
		(asserts! (is-eq reward-token (get token-y (try! (contract-call? 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01 get-pool-details-by-id pool-id)))) err-invalid-reward-token)
		(and (> reward-amount u0) (try! (contract-call? reward-token-trait transfer-fixed reward-amount tx-sender (as-contract tx-sender) none)))
		(match (get-campaign-registration-by-id-or-fail campaign-id pool-id)
			ok-value (map-set campaign-registrations { campaign-id: campaign-id, pool-id: pool-id } (merge ok-value { reward-amount: (+ (get reward-amount ok-value) reward-amount) }))
			err-value (map-set campaign-registrations { campaign-id: campaign-id, pool-id: pool-id } { reward-token: reward-token, reward-amount: reward-amount, total-staked: u0 }))
		(and (is-none (index-of registered-pools pool-id)) 
			(map-set campaign-registered-pools campaign-id (unwrap! (as-max-len? (append registered-pools pool-id) u200) err-invalid-input)))
		(print { notification: "register-for-campaign", payload: { sender: tx-sender, campaign-id: campaign-id, pool-id: pool-id, reward-token: reward-token, reward-amount-added: reward-amount }})
		(ok true)))

;; New public function for voting
(define-public (vote-campaign (campaign-id uint) (votes (list 200 { pool-id: uint, votes: uint })))
  (let (
    (campaign (unwrap! (map-get? campaigns campaign-id) err-invalid-campaign-id))
    (current-timestamp (unwrap! (block-timestamp) err-get-block-info))
    (voter-power (unwrap! (voting-power campaign-id tx-sender) err-invalid-input))
    (total-votes (fold + (map get-votes votes) u0)))
    
    (asserts! (< current-timestamp (get stake-end campaign)) err-stake-end-passed)
    (asserts! (not (get voted voter-power)) err-not-authorized)
    (asserts! (<= total-votes (get voting-power voter-power)) err-invalid-input)
    
    (fold update-pool-votes votes campaign-id)
    (map-set campaign-voted { campaign-id: campaign-id, voter: tx-sender } true)
    (map-set campaign-total-vote campaign-id (+ (default-to u0 (map-get? campaign-total-vote campaign-id)) total-votes))
    
    (print { notification: "vote-campaign", payload: { campaign-id: campaign-id, voter: tx-sender, votes: votes, total-votes: total-votes }})
    (ok true)))

;; governance calls

(define-public (whitelist-pools (pools (list 200 uint)))
    (begin
        (try! (is-dao-or-extension))
        (var-set whitelisted-pools pools)
        (ok true)))

(define-public (create-campaign (registration-cutoff uint) (voting-cutoff uint) (stake-cutoff uint) (stake-end uint) (reward-amount uint) (snapshot-block uint))
  (let (
    (campaign-id (+ (var-get campaign-nonce) u1))
    (snapshot snapshot-block))
    (try! (is-dao-or-extension))
    (asserts! (< registration-cutoff voting-cutoff) err-invalid-input)
    (asserts! (< voting-cutoff stake-cutoff) err-invalid-input)
    (asserts! (< stake-cutoff stake-end) err-invalid-input)
    (map-set campaigns campaign-id { 
      registration-cutoff: registration-cutoff, 
      voting-cutoff: voting-cutoff,
      stake-cutoff: stake-cutoff, 
      stake-end: stake-end, 
      reward-amount: reward-amount,
      snapshot-block: snapshot
    })
		(print { notification: "create-campaign", payload: { campaign-id: campaign-id, registration-cutoff: registration-cutoff, voting-cutoff: voting-cutoff, stake-cutoff: stake-cutoff, stake-end: stake-end, reward-amount: reward-amount, snapshot-block: snapshot }})
    (var-set campaign-nonce campaign-id)
    (ok campaign-id)))

(define-public (transfer-token (token-trait <ft-trait>) (amount uint) (recipient principal))
	(begin 
		(try! (is-dao-or-extension))
		(as-contract (contract-call? token-trait transfer-fixed amount tx-sender recipient none))))

(define-public (update-campaign (campaign-id uint) (details { registration-cutoff: uint, voting-cutoff: uint, stake-cutoff: uint, stake-end: uint, reward-amount: uint, snapshot-block: uint }))
  (let (
    (campaign-details (try! (get-campaign-or-fail campaign-id))))
    (try! (is-dao-or-extension))
    (asserts! (< (get registration-cutoff details) (get voting-cutoff details)) err-invalid-input)
    (asserts! (< (get voting-cutoff details) (get stake-cutoff details)) err-invalid-input)
    (asserts! (< (get stake-cutoff details) (get stake-end details)) err-invalid-input)      
    (map-set campaigns campaign-id details)
    (print { notification: "update-campaign", payload: { campaign-id: campaign-id, details: details }})
    (ok true)))

;; privileged calls
		
;; private calls

(define-private (check-err (result (response bool uint)) (prior (response bool uint)))
  (match prior ok-value result err-value (err err-value)))

(define-private (mul-down (a uint) (b uint))
    (/ (* a b) ONE_8))

(define-private (div-down (a uint) (b uint))
  (if (is-eq a u0) u0 (/ (* a ONE_8) b)))

(define-private (min (a uint) (b uint))
    (if (<= a b) a b))

(define-private (max (a uint) (b uint))
    (if (>= a b) a b))

;; Helper function to get votes from vote entry
(define-private (get-votes (entry { pool-id: uint, votes: uint }))
  (get votes entry))

;; Helper function to update pool votes
(define-private (update-pool-votes (vote { pool-id: uint, votes: uint }) (campaign-id uint))
  (let (
    (pool-id (get pool-id vote))
    (vote-amount (get votes vote))
    (current-votes (default-to u0 (map-get? campaign-pool-votes { campaign-id: campaign-id, pool-id: pool-id }))))
    (map-set campaign-pool-votes { campaign-id: campaign-id, pool-id: pool-id } (+ current-votes vote-amount))
    campaign-id))  ;; Return the campaign-id to be used in the next iteration

Functions (30)

FunctionAccessArgs
is-dao-or-extensionread-only
block-timestampread-only
get-campaign-nonceread-only
get-campaign-or-failread-onlycampaign-id: uint
get-campaigns-or-fail-manyread-onlycampaign-ids: (list 200 uint
get-campaign-registration-by-id-or-failread-onlycampaign-id: uint, pool-id: uint
get-campaign-registration-by-id-or-fail-manyread-onlycampaign-ids: (list 200 uint
get-campaign-staker-or-defaultread-onlycampaign-id: uint, pool-id: uint, staker: principal
get-campaign-staker-or-default-manyread-onlycampaign-ids: (list 200 uint
get-pool-whitelistedread-onlypool-id: uint
get-whitelisted-poolsread-only
voting-powerread-onlycampaign-id: uint, address: principal
get-campaign-registered-poolsread-onlycampaign-id: uint
get-campaign-summaryread-onlycampaign-id: uint
get-campaign-staker-history-manyread-onlyaddress: principal, campaign-ids: (list 200 uint
stakepublicpool-id: uint, campaign-id: uint, amount: uint
unstakepublicpool-id: uint, campaign-id: uint, reward-token-trait: <ft-trait>
register-for-campaignpublicpool-id: uint, campaign-id: uint, reward-token-trait: <ft-trait>, reward-amount: uint
vote-campaignpubliccampaign-id: uint, votes: (list 200 { pool-id: uint, votes: uint }
whitelist-poolspublicpools: (list 200 uint
create-campaignpublicregistration-cutoff: uint, voting-cutoff: uint, stake-cutoff: uint, stake-end: uint, reward-amount: uint, snapshot-block: uint
transfer-tokenpublictoken-trait: <ft-trait>, amount: uint, recipient: principal
update-campaignpubliccampaign-id: uint, details: { registration-cutoff: uint, voting-cutoff: uint, stake-cutoff: uint, stake-end: uint, reward-amount: uint, snapshot-block: uint }
check-errprivateresult: (response bool uint
mul-downprivatea: uint, b: uint
div-downprivatea: uint, b: uint
minprivatea: uint, b: uint
maxprivatea: uint, b: uint
get-votesprivateentry: { pool-id: uint, votes: uint }
update-pool-votesprivatevote: { pool-id: uint, votes: uint }, campaign-id: uint