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