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 : 300 : (define-public (is-dao-or-extension)
64 [ - ][ + - ]: 309 : (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 : 300 : (define-public (set-voting-duration (new-duration uint))
69 : 309 : (begin
70 : 309 : (try! (is-dao-or-extension))
71 : 309 : (var-set voting-duration new-duration)
72 : 309 : (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 : : )
|