LCOV - code coverage report
Current view: top level - contracts/extensions - bme021-0-market-voting.clar (source / functions) Coverage Total Hit
Test: lcov.info Lines: 60.0 % 115 69
Test Date: 2025-11-05 10:54:00 Functions: 55.6 % 18 10
Branches: 28.6 % 21 6

             Branch data     Line data    Source code
       1                 :             : ;; Title: BME021 Market Voting
       2                 :             : ;; Synopsis:
       3                 :             : ;; Intended for prediction market resolution via community voting.
       4                 :             : ;; Description:
       5                 :             : ;; Market votes are connected to a specific market via the market data hash and
       6                 :             : ;; votes are created via challenges to the market outcome. Any user with a stake in the market
       7                 :             : ;; can challenge the outcome. Voting begins on challenge and runs for a DAO configured window.
       8                 :             : ;; DAO governance voting resolves the market - either confirmaing or changing hte original 
       9                 :             : ;; outcome based on a simple majority.
      10                 :             : ;; Unlike proposal voting - market voting is categorical - voters are voting to select an
      11                 :             : ;; outcome from at least 2 and up to 10 potential outcomes.
      12                 :             : 
      13                 :             : (impl-trait 'SP3JP0N1ZXGASRJ0F7QAHWFPGTVK9T2XNXDB908Z.extension-trait.extension-trait)
      14                 :             : (use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)
      15                 :             : (use-trait ft-trait 'SP2AKWJYC7BNY18W1XXKPGP0YVEK63QJG4793Z2D4.sip-010-trait-ft-standard.sip-010-trait)
      16                 :             : (use-trait prediction-market-trait .prediction-market-trait.prediction-market-trait)
      17                 :             : 
      18                 :             : (define-constant err-unauthorised (err u2100))
      19                 :             : (define-constant err-poll-already-exists (err u2102))
      20                 :             : (define-constant err-unknown-proposal (err u2103))
      21                 :             : (define-constant err-proposal-inactive (err u2105))
      22                 :             : (define-constant err-already-voted (err u2106))
      23                 :             : (define-constant err-proposal-start-no-reached (err u2109))
      24                 :             : (define-constant err-expecting-root (err u2110))
      25                 :             : (define-constant err-invalid-signature (err u2111))
      26                 :             : (define-constant err-proposal-already-concluded (err u2112))
      27                 :             : (define-constant err-end-burn-height-not-reached (err u2113))
      28                 :             : (define-constant err-no-votes-to-return (err u2114))
      29                 :             : (define-constant err-not-concluded (err u2115))
      30                 :             : (define-constant err-invalid-category (err u2116))
      31                 :             : (define-constant err-invalid-extension (err u2117))
      32                 :             : 
      33                 :             : 
      34                 :             : (define-constant structured-data-prefix 0x534950303138)
      35                 :             : (define-constant message-domain-hash (sha256 (unwrap! (to-consensus-buff?
      36                 :             :         {
      37                 :             :                 name: "BigMarket",
      38                 :             :                 version: "1.0.0",
      39                 :             :                 chain-id: chain-id
      40                 :             :         }
      41                 :             :     ) err-unauthorised)
      42                 :             : ))
      43                 :             : 
      44                 :             : (define-constant structured-data-header (concat structured-data-prefix message-domain-hash))
      45                 :             : 
      46                 :             : (define-data-var voting-duration uint u288)
      47                 :             : 
      48                 :             : (define-map resolution-polls
      49                 :             :         {market: principal, market-id: uint}
      50                 :             :         {
      51                 :             :     votes: (list 10 uint), ;; votes for each category. NB with 2 categories the votes at 0 are against and 1 = for and 2+ unused 
      52                 :             :                 end-burn-height: uint,
      53                 :             :                 proposer: principal,
      54                 :             :                 concluded: bool,
      55                 :             :     num-categories: uint,
      56                 :             :     winning-category: (optional uint),
      57                 :             :         }
      58                 :             : )
      59                 :             : (define-map member-total-votes {market-id: uint, voter: principal} uint)
      60                 :             : 
      61                 :             : ;; --- Authorisation check
      62                 :             : 
      63                 :         289 : (define-public (is-dao-or-extension)
      64    [ - ][ +  - ]:         298 :         (ok (asserts! (or (is-eq tx-sender .bigmarket-dao) (contract-call? .bigmarket-dao is-extension contract-caller)) err-unauthorised))
      65                 :             : )
      66                 :             : 
      67                 :             : ;; --- Internal DAO functions
      68                 :         289 : (define-public (set-voting-duration (new-duration uint))
      69                 :         298 :   (begin
      70                 :         298 :     (try! (is-dao-or-extension))
      71                 :         298 :     (var-set voting-duration new-duration)
      72                 :         298 :     (ok true)
      73                 :             :   )
      74                 :             : )
      75                 :             : 
      76                 :             : ;; called by a user to begin dispute resolution process.
      77                 :             : ;; access conditions for this action are determined in the contract-call to the market contract.
      78                 :           9 : (define-public (create-market-vote
      79                 :             :     (market <prediction-market-trait>)
      80                 :             :     (market-id uint)
      81                 :             :     (empty-votes (list 10 uint))
      82                 :             :     (num-categories uint)
      83                 :             :   )
      84                 :           9 :   (let
      85                 :             :     (
      86                 :           9 :       (original-sender tx-sender)
      87                 :             :     )
      88                 :             : 
      89                 :             :     ;; Verify that the market is a registered extension
      90            [ - ]:           9 :     (asserts! (contract-call? .bigmarket-dao is-extension (contract-of market)) err-invalid-extension)
      91                 :             :     ;; ensure no market vote already exists
      92            [ - ]:           9 :     (asserts! (is-none (map-get? resolution-polls {market-id: market-id, market: (contract-of market)})) err-poll-already-exists)
      93            [ - ]:           9 :     (asserts! (is-eq (len empty-votes) num-categories) err-poll-already-exists)
      94                 :             :                 ;; see market for access rules. 
      95                 :           9 :     (try! (as-contract (contract-call? market dispute-resolution market-id original-sender num-categories)))
      96                 :             : 
      97                 :             :     ;; Register the poll
      98                 :           9 :     (map-set resolution-polls {market-id: market-id, market: (contract-of market)}
      99                 :             :       {
     100                 :           9 :       votes: empty-votes,
     101                 :           9 :       end-burn-height: (+ burn-block-height (var-get voting-duration)),
     102                 :           9 :       proposer: tx-sender,
     103                 :           9 :       concluded: false,
     104                 :           9 :       num-categories: num-categories,
     105                 :           9 :       winning-category: none}
     106                 :             :     )
     107                 :             : 
     108                 :             :     ;; Emit an event for the new poll
     109                 :           9 :     (print {event: "create-market-vote", market-id: market-id, proposer: tx-sender, market: market})
     110                 :           9 :     (ok true)
     111                 :             :   )
     112                 :             : )
     113                 :             : 
     114                 :             : ;; --- Public functions
     115                 :             : 
     116                 :           8 : (define-read-only (get-poll-data (market principal) (market-id uint))
     117                 :          16 :         (map-get? resolution-polls {market-id: market-id, market: market})
     118                 :             : )
     119                 :             : 
     120                 :             : 
     121                 :             : ;; Votes
     122                 :             : 
     123                 :           5 : (define-public (vote
     124                 :             :     (market principal)
     125                 :             :     (market-id uint) 
     126                 :             :     (category-for uint)
     127                 :             :     (amount uint) 
     128                 :             :     (prev-market-id (optional uint))
     129                 :             :   )
     130                 :             :   ;; Process the vote using shared logic
     131                 :          10 :   (process-market-vote market market-id tx-sender category-for amount false prev-market-id)
     132                 :             : )
     133                 :             : 
     134                 :             : 
     135                 :           0 : (define-public (batch-vote (votes (list 50 {message: (tuple 
     136                 :             :                                                 (market principal)
     137                 :             :                                                 (market-id uint)
     138                 :             :                                                 (attestation (string-ascii 100)) 
     139                 :             :                                                 (timestamp uint) 
     140                 :             :                                                 (category-for uint)
     141                 :             :                                                 (amount uint)
     142                 :             :                                                 (voter principal)
     143                 :             :                                                 (prev-market-id (optional uint))),
     144                 :             :                                    signature: (buff 65)})))
     145                 :           0 :   (begin
     146                 :           0 :     (ok (fold fold-vote votes u0))
     147                 :             :   )
     148                 :             : )
     149                 :             : 
     150                 :           0 : (define-private (fold-vote  (input-vote {message: (tuple 
     151                 :             :                                                 (market principal)
     152                 :             :                                                 (market-id uint)
     153                 :             :                                                 (attestation (string-ascii 100)) 
     154                 :             :                                                 (timestamp uint) 
     155                 :             :                                                 (category-for uint)
     156                 :             :                                                 (amount uint)
     157                 :             :                                                 (voter principal)
     158                 :             :                                                 (prev-market-id (optional uint))),
     159                 :             :                                      signature: (buff 65)}) (current uint))
     160                 :           0 :   (let
     161                 :             :     (
     162                 :           0 :       (vote-result (process-vote input-vote))
     163                 :             :     )
     164                 :           0 :     (if (is-ok vote-result)
     165            [ - ]:           0 :         (if (unwrap! vote-result u0)
     166            [ - ]:           0 :             (+ current u1)
     167            [ - ]:           0 :             current) 
     168            [ - ]:           0 :         current)
     169                 :             :   )
     170                 :             : )
     171                 :             : 
     172                 :           0 : (define-private (process-vote
     173                 :             :     (input-vote {message: (tuple 
     174                 :             :                             (market principal)
     175                 :             :                             (market-id uint)
     176                 :             :                             (attestation (string-ascii 100)) 
     177                 :             :                             (timestamp uint) 
     178                 :             :                             (category-for uint)
     179                 :             :                             (amount uint)
     180                 :             :                             (voter principal)
     181                 :             :                             (prev-market-id (optional uint))),
     182                 :             :                  signature: (buff 65)}))
     183                 :           0 :   (let
     184                 :             :       (
     185                 :             :         ;; Extract relevant fields from the message
     186                 :           0 :         (message-data (get message input-vote))
     187                 :           0 :         (attestation (get attestation message-data))
     188                 :           0 :         (timestamp (get timestamp message-data))
     189                 :           0 :         (market (get market message-data))
     190                 :           0 :         (market-id (get market-id message-data))
     191                 :           0 :         (voter (get voter message-data))
     192                 :           0 :         (category-for (get category-for message-data))
     193                 :           0 :         (amount (get amount message-data))
     194                 :             :         ;; Verify the signature
     195                 :           0 :         (message (tuple (attestation attestation) (market-id market-id) (timestamp timestamp) (vote (get category-for message-data))))
     196                 :           0 :         (structured-data-hash (sha256 (unwrap! (to-consensus-buff? message) err-unauthorised)))
     197                 :           0 :         (is-valid-sig (verify-signed-structured-data structured-data-hash (get signature input-vote) voter))
     198                 :             :       )
     199                 :           0 :     (if is-valid-sig
     200            [ - ]:           0 :         (process-market-vote market market-id voter category-for amount true (get prev-market-id message-data) )
     201            [ - ]:           0 :         (ok false)) ;; Invalid signature
     202                 :             :   ))
     203                 :             : 
     204                 :             : 
     205                 :           5 : (define-private (process-market-vote
     206                 :             :     (market principal) ;; the market contract
     207                 :             :     (market-id uint)   ;; the market id
     208                 :             :     (voter principal)         ;; The voter's principal
     209                 :             :     (category-for uint)        ;; category voting for (with two categories, we get simple "for" or "against" binary vote)
     210                 :             :     (amount uint)             ;; voting power
     211                 :             :     (sip18 bool)              ;; sip18 message vote or tx vote
     212                 :             :     (prev-market-id (optional uint))
     213                 :             :   )
     214                 :          10 :   (let
     215                 :             :       (
     216                 :             :         ;; Fetch the poll data
     217                 :          10 :         (poll-data (unwrap! (map-get? resolution-polls {market-id: market-id, market: market}) err-unknown-proposal))
     218                 :          10 :         (num-categories (get num-categories poll-data))
     219                 :          10 :         (current-votes (get votes poll-data))
     220                 :             :       )
     221                 :          10 :     (begin
     222                 :             :       ;; reclaim previously locked tokens
     223         [ -  + ]:          10 :                 (if (is-some prev-market-id) (try! (reclaim-votes market prev-market-id)) true)
     224                 :             : 
     225                 :             :       ;; Ensure the voting period is active
     226            [ + ]:          10 :       (asserts! (< burn-block-height (get end-burn-height poll-data)) err-proposal-inactive)
     227                 :             : 
     228                 :             :       ;; passed category exists
     229            [ - ]:           9 :       (asserts! (< category-for num-categories) err-invalid-category)
     230                 :             : 
     231                 :             :       ;; Record the vote
     232                 :           9 :       (map-set member-total-votes {market-id: market-id, voter: voter}
     233                 :           9 :         (+ (get-current-total-votes market-id voter) amount)
     234                 :             :       )
     235                 :             : 
     236                 :             :       ;; update market voting power
     237                 :           9 :       (map-set resolution-polls {market-id: market-id, market: market}
     238                 :           9 :         (merge poll-data 
     239                 :           9 :           {votes: (unwrap! (replace-at? current-votes category-for (+ (unwrap! (element-at? current-votes category-for) err-invalid-category) amount)) err-invalid-category)}
     240                 :             :         )
     241                 :             :       )
     242                 :             : 
     243                 :             :       ;; Emit an event for the vote
     244                 :           9 :       (print {event: "market-vote", market-id: market-id, voter: voter, category-for: category-for, sip18: sip18, amount: amount, prev-market-id: prev-market-id})
     245                 :             : 
     246                 :           9 :                   (contract-call? .bme000-0-governance-token bmg-lock amount voter)
     247                 :             :     )
     248                 :             :   ))
     249                 :             : 
     250                 :             : 
     251                 :           4 : (define-read-only (get-current-total-votes (market-id uint) (voter principal))
     252                 :           9 :         (default-to u0 (map-get? member-total-votes {market-id: market-id, voter: voter}))
     253                 :             : )
     254                 :             : 
     255                 :           0 : (define-read-only (verify-signature (hash (buff 32)) (signature (buff 65)) (signer principal))
     256                 :           0 :         (is-eq (principal-of? (unwrap! (secp256k1-recover? hash signature) false)) (ok signer))
     257                 :             : )
     258                 :             : 
     259                 :           0 : (define-read-only (verify-signed-structured-data (structured-data-hash (buff 32)) (signature (buff 65)) (signer principal))
     260                 :           0 :         (verify-signature (sha256 (concat structured-data-header structured-data-hash)) signature signer)
     261                 :             : )
     262                 :             : 
     263                 :             : ;; Conclusion
     264                 :             : 
     265                 :           0 : (define-read-only (get-poll-status (market principal) (market-id uint))
     266                 :           0 :     (let
     267                 :             :         (
     268                 :           0 :             (poll-data (unwrap! (map-get? resolution-polls {market-id: market-id, market: market}) err-unknown-proposal))
     269                 :           0 :             (is-active (< burn-block-height (get end-burn-height poll-data)))
     270                 :             :         )
     271                 :           0 :         (ok {active: is-active, concluded: (get concluded poll-data), votes: (get votes poll-data)})
     272                 :             :     )
     273                 :             : )
     274                 :             :     
     275                 :             : 
     276                 :           4 : (define-public (conclude-market-vote (market <prediction-market-trait>) (market-id uint))
     277                 :           5 :         (let
     278                 :             :                 (
     279                 :           5 :       (poll-data (unwrap! (map-get? resolution-polls {market-id: market-id, market: (contract-of market)}) err-unknown-proposal))
     280                 :           5 :       (votes (get votes poll-data))
     281                 :           5 :       (winning-category (get max-index (find-max-category votes)))
     282                 :           5 :       (total-votes (fold + votes u0))
     283                 :           5 :       (winning-votes (unwrap! (element-at? votes winning-category) err-already-voted))
     284                 :           5 :       (result (try! (contract-call? market resolve-market-vote market-id winning-category)))
     285                 :             :                 )
     286            [ - ]:           5 :                 (asserts! (not (get concluded poll-data)) err-proposal-already-concluded)
     287            [ + ]:           5 :                 (asserts! (>= burn-block-height (get end-burn-height poll-data)) err-end-burn-height-not-reached)
     288                 :           3 :                 (map-set resolution-polls {market-id: market-id, market: (contract-of market)} (merge poll-data {concluded: true, winning-category: (some winning-category)}))
     289                 :           3 :                 (print {event: "conclude-market-vote", market-id: market-id, winning-category: winning-category, result: result})
     290                 :           3 :     (try! (contract-call? .bme030-0-reputation-token mint tx-sender u13 u2))
     291                 :           3 :                 (ok winning-category)
     292                 :             :         )
     293                 :             : )
     294                 :             : 
     295                 :           0 : (define-public (reclaim-votes (market principal) (id (optional uint)))
     296                 :           0 :         (let
     297                 :             :                 (
     298                 :           0 :                         (market-id (unwrap! id err-unknown-proposal))
     299                 :           0 :       (poll-data (unwrap! (map-get? resolution-polls {market: market, market-id: market-id}) err-unknown-proposal))
     300                 :           0 :                         (votes (unwrap! (map-get? member-total-votes {market-id: market-id, voter: tx-sender}) err-no-votes-to-return))
     301                 :             :                 )
     302            [ - ]:           0 :                 (asserts! (get concluded poll-data) err-not-concluded)
     303                 :           0 :                 (map-delete member-total-votes {market-id: market-id, voter: tx-sender})
     304                 :           0 :                 (contract-call? .bme000-0-governance-token bmg-unlock votes tx-sender)
     305                 :             :         )
     306                 :             : )
     307                 :             : 
     308                 :             : ;; --- Extension callback
     309                 :           0 : (define-public (callback (sender principal) (memo (buff 34)))
     310                 :           0 :         (ok true)
     311                 :             : )
     312                 :             : 
     313                 :             : 
     314                 :           4 : (define-private (find-max-category (votes (list 10 uint)))
     315                 :           5 :   (fold find-max-iter votes {max-votes: u0, max-index: u0, current-index: u0})
     316                 :             : )
     317                 :             : 
     318                 :           4 : (define-private (find-max-iter (current-votes uint) (acc (tuple (max-votes uint) (max-index uint) (current-index uint))))
     319                 :          10 :   (let
     320                 :             :     (
     321                 :          10 :       (max-votes (get max-votes acc))  ;; Extract highest vote count so far
     322                 :          10 :       (max-index (get max-index acc))  ;; Extract category index with highest votes
     323                 :          10 :       (current-index (get current-index acc))  ;; Track current category index
     324                 :             :     )
     325                 :          10 :     (if (> current-votes max-votes)
     326            [ + ]:           2 :       (tuple (max-votes current-votes) (max-index current-index) (current-index (+ current-index u1)))  ;; Update max
     327            [ + ]:           8 :       (tuple (max-votes max-votes) (max-index max-index) (current-index (+ current-index u1)))  ;; Keep previous max
     328                 :             :     )
     329                 :             :   )
     330                 :             : )
        

Generated by: LCOV version 2.3.2-1