Branch data Line data Source code
1 : : ;; Title: BME024 CPMM Scalar Market Predictions
2 : : ;; Synopsis:
3 : : ;; Implements CPMM scalar prediciton markets with pyth oracle resolution and DAO.
4 : : ;; Description:
5 : : ;; Scalar markets differ from binary/categorical markets (see bme024-0-market-predicting)
6 : : ;; in the type of categories and the mechanism for resolution:
7 : : ;; Firstly, the categories are contiguous ranges of numbers with a min and max value. The winning
8 : : ;; category is decided by the range that the outcome selects. Secondly, scalar market outcomes
9 : : ;; are determined by on-chain oracles. This contract uses the Pyth oracle for selecting from possible outcomes.
10 : : ;; Market creation can be gated via market proof and a market creator can
11 : : ;; set their own fee up to a max fee amount determined by the DAO.
12 : : ;; Anyone with the required token can buy shares. Resolution process begins via a call gated
13 : : ;; to the DAO controlled resolution agent address. The resolution can be challenged by anyone with a stake in the market
14 : : ;; If a challenge is made the dispute resolution process begins which requires a DAO vote
15 : : ;; to resolve - the outcome of the vote resolve the market and sets the outcome.
16 : : ;; If the dispute window passes without challenge or once the vote concludes the market is fully
17 : : ;; resolved and claims can then be made.
18 : : ;; Optional hedge strategy - the execute-hedge strategy can be run during market cool down period. The execute-hedge
19 : : ;; function will call the market specific hedge strategy if supplied or the default startegy otherwise.
20 : : ;; The hedge strategy can be switched off by the dao.
21 : :
22 : : (use-trait ft-token 'SP2AKWJYC7BNY18W1XXKPGP0YVEK63QJG4793Z2D4.sip-010-trait-ft-standard.sip-010-trait)
23 : : (impl-trait .prediction-market-trait.prediction-market-trait)
24 : : (use-trait hedge-trait .hedge-trait.hedge-trait)
25 : : (use-trait ft-velar-token 'SP2AKWJYC7BNY18W1XXKPGP0YVEK63QJG4793Z2D4.sip-010-trait-ft-standard.sip-010-trait)
26 : : (impl-trait 'SP3JP0N1ZXGASRJ0F7QAHWFPGTVK9T2XNXDB908Z.extension-trait.extension-trait)
27 : :
28 : : ;; ---------------- CONSTANTS & TYPES ----------------
29 : : ;; Market Types (2 => range based markets)
30 : : (define-constant MARKET_TYPE u2)
31 : :
32 : : ;; TODO Update resolve-market to reference correct pyth contract and remove local pyth from deployment
33 : : ;; PYTH_ORACLE 'STR738QQX1PVTM6WTDF833Z18T8R0ZB791TCNEFM.pyth-storage-v4
34 : : ;; PYTH_ORACLE 'SP1CGXWEAMG6P6FT04W66NVGJ7PQWMDAC19R7PJ0Y.pyth-oracle-v4
35 : :
36 : : (define-constant DEFAULT_MARKET_DURATION u144) ;; ~1 day in Bitcoin blocks
37 : : (define-constant DEFAULT_COOL_DOWN_PERIOD u144) ;; ~1 day in Bitcoin blocks
38 : : (define-constant SCALE u1000000)
39 : :
40 : : (define-constant RESOLUTION_OPEN u0)
41 : : (define-constant RESOLUTION_RESOLVING u1)
42 : : (define-constant RESOLUTION_DISPUTED u2)
43 : : (define-constant RESOLUTION_RESOLVED u3)
44 : :
45 : : (define-constant err-unauthorised (err u10000))
46 : : (define-constant err-invalid-market-type (err u10001))
47 : : (define-constant err-amount-too-low (err u10002))
48 : : (define-constant err-wrong-market-type (err u10003))
49 : : (define-constant err-already-concluded (err u10004))
50 : : (define-constant err-market-not-found (err u10005))
51 : : (define-constant err-user-not-winner-or-claimed (err u10006))
52 : : (define-constant err-user-not-staked (err u10008))
53 : : (define-constant err-market-not-concluded (err u10009))
54 : : (define-constant err-insufficient-balance (err u10011))
55 : : (define-constant err-insufficient-contract-balance (err u10012))
56 : : (define-constant err-user-share-is-zero (err u10013))
57 : : (define-constant err-disputer-must-have-stake (err u10015))
58 : : (define-constant err-dispute-window-elapsed (err u10016))
59 : : (define-constant err-market-not-resolving (err u10017))
60 : : (define-constant err-market-not-open (err u10018))
61 : : (define-constant err-dispute-window-not-elapsed (err u10019))
62 : : (define-constant err-market-wrong-state (err u10020))
63 : : (define-constant err-invalid-token (err u10021))
64 : : (define-constant err-max-market-fee-bips-exceeded (err u10022))
65 : : (define-constant err-category-not-found (err u10023))
66 : : (define-constant err-too-few-categories (err u10024))
67 : : (define-constant err-element-expected (err u10025))
68 : : (define-constant err-winning-stake-not-zero (err u10026))
69 : : (define-constant err-losing-stake-is-zero (err u10027))
70 : : (define-constant err-amount-too-high (err u10029))
71 : : (define-constant err-fee-too-high (err u10030))
72 : : (define-constant err-slippage-too-high (err u10031))
73 : : (define-constant err-seed-amount-not-divisible (err u10032))
74 : : (define-constant err-overbuy (err u10034))
75 : : (define-constant err-token-not-configured (err u10035))
76 : : (define-constant err-seed-too-small (err u10036))
77 : : (define-constant err-already-hedged (err u10037))
78 : : (define-constant err-hedging-disabled (err u10038))
79 : : (define-constant err-insufficient-liquidity (err u11041))
80 : : (define-constant err-arithmetic (err u11043))
81 : : (define-constant err-oracle-start-price (err u10039))
82 : : (define-constant err-band-not-set (err u10040))
83 : : (define-constant err-oracle-stale (err u10150))
84 : : (define-constant err-oracle-uncertain (err u10151))
85 : : (define-constant err-oracle-volatile (err u10152))
86 : : (define-constant err-oracle-no-fallback (err u10153))
87 : : (define-constant err-oracle-zero-price (err u10154))
88 : : (define-constant err-oracle-zero-delta (err u10155))
89 : : (define-constant err-invalid-param (err u10156))
90 : : (define-constant err-oracle-call-failed (err u10157))
91 : : (define-constant err-oracle-expo (err u10158))
92 : : (define-constant err-hedge-window (err u10101))
93 : : (define-constant err-market-ended (err u10102))
94 : :
95 : : (define-constant marketplace .bme040-0-shares-marketplace)
96 : : (define-constant MIN_POOL u1)
97 : :
98 : : (define-data-var market-counter uint u0)
99 : : (define-data-var dispute-window-length uint u144)
100 : : (define-data-var dev-fee-bips uint u100)
101 : : (define-data-var market-fee-bips-max uint u1000)
102 : : (define-data-var dev-fund principal tx-sender)
103 : : (define-data-var resolution-agent principal tx-sender)
104 : : (define-data-var dao-treasury principal tx-sender)
105 : : (define-data-var creation-gated bool true)
106 : : (define-data-var resolution-timeout uint u1000) ;; 1000 blocks (~9 days)
107 : : (define-data-var default-hedge-executor principal .bme032-0-scalar-strategy-hedge)
108 : : (define-data-var hedging-enabled bool true)
109 : :
110 : : (define-data-var max-staleness-secs uint u900) ;; optional defense-in-depth
111 : : (define-data-var max-conf-bips uint u200) ;; 2%
112 : : (define-data-var max-move-bips uint u2000) ;; 20%
113 : :
114 : : ;; e.g. band-bips = 500 => 5.00%
115 : : (define-map price-band-widths {feed-id: (buff 32)} {band-bips: uint})
116 : : (define-map manual-fallback-price uint uint)
117 : :
118 : : ;; Data structure for each Market
119 : : ;; outcome: winning category
120 : : (define-map markets
121 : : uint
122 : : {
123 : : market-data-hash: (buff 32),
124 : : token: principal,
125 : : treasury: principal,
126 : : creator: principal,
127 : : market-fee-bips: uint,
128 : : resolution-state: uint, ;; "open", "resolving", "disputed", "concluded"
129 : : resolution-burn-height: uint,
130 : : categories: (list 6 {min: uint, max: uint}), ;; Min (inclusive) and Max (exclusive)
131 : : stakes: (list 10 uint), ;; Total staked per category - shares
132 : : stake-tokens: (list 10 uint), ;; Total staked per category - tokens
133 : : outcome: (optional uint),
134 : : concluded: bool,
135 : : market-start: uint,
136 : : market-duration: uint,
137 : : cool-down-period: uint,
138 : : hedge-executor: (optional principal),
139 : : hedged: bool,
140 : : price-feed-id: (buff 32), ;; Pyth price feed ID
141 : : price-outcome: (optional uint),
142 : : start-price: uint,
143 : : }
144 : : )
145 : : ;; defines the minimum liquidity a market creator needs to provide
146 : : (define-map token-minimum-seed {token: principal} uint)
147 : :
148 : : ;; tracks the amount of shares the user owns per market / category
149 : : (define-map stake-balances
150 : : { market-id: uint, user: principal }
151 : : (list 10 uint)
152 : : )
153 : : ;; tracks the cost of shares to the user per market / category
154 : : (define-map token-balances
155 : : { market-id: uint, user: principal }
156 : : (list 10 uint)
157 : : )
158 : : (define-map allowed-tokens principal bool)
159 : :
160 : : ;; ---------------- access control ----------------
161 : 421 : (define-public (is-dao-or-extension)
162 [ - ][ + + ]: 8977 : (ok (asserts! (or (is-eq tx-sender .bigmarket-dao) (contract-call? .bigmarket-dao is-extension contract-caller)) err-unauthorised))
163 : : )
164 : :
165 : : ;; ---------------- getters / setters ----------------
166 : 421 : (define-public (set-allowed-token (token principal) (enabled bool))
167 : 1684 : (begin
168 : 1684 : (try! (is-dao-or-extension))
169 : 1684 : (print {event: "allowed-token", token: token, enabled: enabled})
170 : 1684 : (ok (map-set allowed-tokens token enabled))
171 : : )
172 : : )
173 : 57 : (define-read-only (is-allowed-token (token principal))
174 : 65 : (default-to false (map-get? allowed-tokens token))
175 : : )
176 : :
177 : 421 : (define-public (set-dispute-window-length (length uint))
178 : 421 : (begin
179 : 421 : (try! (is-dao-or-extension))
180 : 421 : (var-set dispute-window-length length)
181 : 421 : (ok true)
182 : : )
183 : : )
184 : :
185 : 421 : (define-public (set-default-hedge-executor (p principal))
186 : 440 : (begin
187 : 440 : (try! (is-dao-or-extension))
188 : 440 : (var-set default-hedge-executor p)
189 : 440 : (print {event: "default-hedge-executor", default-hedge-executor: p})
190 : 440 : (ok true)
191 : : )
192 : : )
193 : :
194 : 3 : (define-public (set-hedging-enabled (enabled bool))
195 : 3 : (begin
196 : 3 : (try! (is-dao-or-extension))
197 : 3 : (var-set hedging-enabled enabled)
198 : 3 : (ok enabled)
199 : : )
200 : : )
201 : 0 : (define-read-only (is-hedging-enabled) (var-get hedging-enabled))
202 : :
203 : 421 : (define-public (set-price-band-width (feed-id (buff 32)) (band-bips uint))
204 : 1690 : (begin
205 : 1690 : (try! (is-dao-or-extension))
206 : 1690 : (map-set price-band-widths {feed-id: feed-id} {band-bips: band-bips})
207 : 1690 : (print {event: "price-band-width", feed-id: feed-id, precent: band-bips})
208 : 1690 : (ok true)
209 : : )
210 : : )
211 : 1 : (define-read-only (get-price-band-width (feed-id (buff 32)))
212 : 1 : (ok (map-get? price-band-widths {feed-id: feed-id}))
213 : : )
214 : :
215 : 421 : (define-public (set-creation-gated (gated bool))
216 : 421 : (begin
217 : 421 : (try! (is-dao-or-extension))
218 : 421 : (var-set creation-gated gated)
219 : 421 : (ok true)
220 : : )
221 : : )
222 : :
223 : 421 : (define-public (set-resolution-agent (new-agent principal))
224 : 421 : (begin
225 : 421 : (try! (is-dao-or-extension))
226 : 421 : (var-set resolution-agent new-agent)
227 : 421 : (ok true)
228 : : )
229 : : )
230 : :
231 : 421 : (define-public (set-dev-fee-bips (new-fee uint))
232 : 423 : (begin
233 [ - ]: 423 : (asserts! (<= new-fee u1000) err-max-market-fee-bips-exceeded)
234 : 423 : (try! (is-dao-or-extension))
235 : 423 : (var-set dev-fee-bips new-fee)
236 : 423 : (ok true)
237 : : )
238 : : )
239 : :
240 : 421 : (define-public (set-market-fee-bips-max (new-fee uint))
241 : 421 : (begin
242 [ - ]: 421 : (asserts! (<= new-fee u1000) err-max-market-fee-bips-exceeded)
243 : 421 : (try! (is-dao-or-extension))
244 : 421 : (var-set market-fee-bips-max new-fee)
245 : 421 : (ok true)
246 : : )
247 : : )
248 : :
249 : 421 : (define-public (set-token-minimum-seed (token principal) (min uint))
250 : 1684 : (begin
251 : 1684 : (try! (is-dao-or-extension))
252 : 1684 : (map-set token-minimum-seed {token: token} min)
253 : 1684 : (ok true)
254 : : )
255 : : )
256 : :
257 : 0 : (define-read-only (get-token-minimum-seed (seed-token principal))
258 : 0 : (ok (map-get? token-minimum-seed {token: seed-token}))
259 : : )
260 : :
261 : 421 : (define-public (set-dev-fund (new-dev-fund principal))
262 : 421 : (begin
263 : 421 : (try! (is-dao-or-extension))
264 : 421 : (var-set dev-fund new-dev-fund)
265 : 421 : (ok true)
266 : : )
267 : : )
268 : :
269 : 421 : (define-public (set-dao-treasury (new-dao-treasury principal))
270 : 423 : (begin
271 : 423 : (try! (is-dao-or-extension))
272 : 423 : (var-set dao-treasury new-dao-treasury)
273 : 423 : (ok true)
274 : : )
275 : : )
276 : :
277 : 421 : (define-public (set-max-staleness (secs uint))
278 : 426 : (begin
279 : 426 : (try! (is-dao-or-extension))
280 [ - ][ + + ]: 426 : (asserts! (and (> secs u60) (< secs u86400000)) err-invalid-param)
281 : 426 : (var-set max-staleness-secs secs)
282 : 426 : (ok true)
283 : : )
284 : : )
285 : :
286 : 2 : (define-public (set-max-conf-bips (bips uint))
287 : 4 : (begin
288 : 4 : (try! (is-dao-or-extension))
289 [ + ]: 4 : (asserts! (<= bips u1000) err-invalid-param)
290 : 2 : (var-set max-conf-bips bips)
291 : 2 : (ok true)
292 : : )
293 : : )
294 : :
295 : 5 : (define-public (set-max-move-bips (bips uint))
296 : 5 : (begin
297 : 5 : (try! (is-dao-or-extension))
298 [ - ]: 5 : (asserts! (<= bips u10000) err-invalid-param)
299 : 5 : (var-set max-move-bips bips)
300 : 5 : (ok true)
301 : : )
302 : : )
303 : :
304 : 12 : (define-read-only (get-market-data (market-id uint))
305 : 18 : (map-get? markets market-id)
306 : : )
307 : :
308 : 2 : (define-read-only (get-stake-balances (market-id uint) (user principal))
309 : 2 : (ok (default-to (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0) (map-get? stake-balances {market-id: market-id, user: user})))
310 : : )
311 : :
312 : 2 : (define-read-only (get-token-balances (market-id uint) (user principal))
313 : 2 : (ok (default-to (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0) (map-get? token-balances {market-id: market-id, user: user})))
314 : : )
315 : :
316 : : ;; ---------------- public functions ----------------
317 : :
318 : 57 : (define-public (create-market
319 : : (fee-bips (optional uint))
320 : : (token <ft-token>)
321 : : (market-data-hash (buff 32))
322 : : (proof (list 10 (tuple (position bool) (hash (buff 32)))))
323 : : (treasury principal)
324 : : (market-duration (optional uint))
325 : : (cool-down-period (optional uint))
326 : : (price-feed-id (buff 32))
327 : : (seed-amount uint)
328 : : (hedge-executor (optional principal))
329 : : )
330 : 67 : (let (
331 : 67 : (creator tx-sender)
332 : 67 : (new-id (var-get market-counter))
333 : 67 : (market-fee-bips (default-to u0 fee-bips))
334 : 67 : (market-duration-final (default-to DEFAULT_MARKET_DURATION market-duration))
335 : 67 : (cool-down-final (default-to DEFAULT_COOL_DOWN_PERIOD cool-down-period))
336 : 67 : (current-block burn-block-height)
337 : 67 : (start-price (try! (get-current-price-safe price-feed-id)))
338 : : ;;(start-price (match start-price-wrapped price price price u0))
339 : 65 : (band-bips (get band-bips (unwrap! (map-get? price-band-widths {feed-id: price-feed-id}) err-band-not-set)))
340 : :
341 : 65 : (delta (/ (* start-price band-bips) u10000))
342 : 65 : (categories (category-bands start-price delta))
343 : 65 : (num-categories (len categories))
344 : : ;; NOTE: seed is evenly divided with rounding error discarded
345 : 65 : (seed (/ (* seed-amount SCALE) (* num-categories SCALE)))
346 : 65 : (user-stake-list (list seed seed seed seed seed seed seed seed seed seed))
347 : 65 : (share-list (zero-after-n user-stake-list num-categories))
348 : : )
349 [ - ]: 65 : (asserts! (> start-price u0) err-oracle-zero-price)
350 [ - ]: 65 : (asserts! (> band-bips u0) err-band-not-set)
351 [ - ]: 65 : (asserts! (> delta u0) err-oracle-zero-delta)
352 [ - ]: 65 : (asserts! (> market-duration-final u10) err-market-not-found)
353 [ - ]: 65 : (asserts! (> cool-down-final u10) err-market-not-found)
354 : :
355 [ - ]: 65 : (asserts! (> (len categories) u1) err-too-few-categories)
356 [ - ]: 65 : (asserts! (<= market-fee-bips (var-get market-fee-bips-max)) err-max-market-fee-bips-exceeded)
357 : : ;; ensure the trading token is allowed
358 [ - ]: 65 : (asserts! (is-allowed-token (contract-of token)) err-invalid-token)
359 : :
360 : : ;; ensure enough liquidity
361 [ - ]: 65 : (asserts! (>= seed-amount (unwrap! (map-get? token-minimum-seed {token: (contract-of token)}) err-token-not-configured)) err-seed-too-small)
362 : : ;; liquidity floor guards (for CPMM safety)
363 [ - ]: 65 : (asserts! (>= seed MIN_POOL) err-insufficient-liquidity)
364 [ - ]: 65 : (asserts! (>= seed-amount (* num-categories MIN_POOL)) err-insufficient-liquidity) ;; avoid rounding below floor
365 : :
366 : : ;; Transfer single winning portion of seed to market contract to fund claims
367 : 65 : (try! (contract-call? token transfer seed-amount tx-sender (as-contract tx-sender) none))
368 : :
369 : : ;; ensure the user is allowed to create if gating by merkle proof is required
370 [ + - ]: 65 : (if (var-get creation-gated) (try! (as-contract (contract-call? .bme022-0-market-gating can-access-by-account creator proof))) true)
371 : :
372 : : ;; dao is assigned the seed liquidity - share and tokens 1:1 at kick off
373 : 65 : (map-set stake-balances {market-id: new-id, user: (var-get dao-treasury)} share-list)
374 : 65 : (map-set token-balances {market-id: new-id, user: (var-get dao-treasury)} share-list)
375 : :
376 : 65 : (map-set markets
377 : 65 : new-id
378 : : {
379 : 65 : market-data-hash: market-data-hash,
380 : 65 : token: (contract-of token),
381 : 65 : treasury: treasury,
382 : 65 : creator: creator,
383 : 65 : market-fee-bips: market-fee-bips,
384 : 65 : resolution-state: RESOLUTION_OPEN,
385 : 65 : resolution-burn-height: u0,
386 : 65 : categories: categories,
387 : 65 : stakes: share-list,
388 : 65 : stake-tokens: share-list, ;; they start out the same
389 : 65 : outcome: none,
390 : 65 : concluded: false,
391 : 65 : market-start: current-block,
392 : 65 : market-duration: market-duration-final,
393 : 65 : cool-down-period: cool-down-final,
394 : 65 : hedge-executor: hedge-executor,
395 : 65 : hedged: false,
396 : 65 : price-feed-id: price-feed-id,
397 : 65 : price-outcome: none,
398 : 65 : start-price: start-price
399 : : }
400 : : )
401 : 65 : (var-set market-counter (+ new-id u1))
402 : 65 : (try! (contract-call? .bme030-0-reputation-token mint tx-sender u2 u8))
403 : 65 : (print {event: "create-market", market-id: new-id, categories: categories, market-fee-bips: market-fee-bips, token: token, market-data-hash: market-data-hash, creator: tx-sender, seed-amount: seed-amount, start-price: start-price })
404 : 65 : (ok new-id)
405 : : )
406 : : )
407 : :
408 : : ;; Read-only: get current price to buy `amount` shares in a category
409 : 2 : (define-read-only (get-share-cost (market-id uint) (index uint) (amount-shares uint))
410 : 2 : (let (
411 : 2 : (market-data (unwrap-panic (map-get? markets market-id)))
412 : 2 : (stake-list (get stakes market-data))
413 : 2 : (selected-pool (unwrap-panic (element-at? stake-list index)))
414 : 2 : (total-pool (fold + stake-list u0))
415 : 2 : (other-pool (- total-pool selected-pool))
416 [ + - ]: 2 : (max-purchase (if (> other-pool MIN_POOL) (- other-pool MIN_POOL) u0))
417 : 2 : (cost (unwrap! (cpmm-cost selected-pool other-pool amount-shares) err-arithmetic))
418 : : )
419 : 2 : (ok { cost: cost, max-purchase: max-purchase })
420 : : )
421 : : )
422 : :
423 : : ;; Compute the token cost to buy `amount-shares` from `selected-pool`,
424 : : ;; given the rest-of-market liquidity `other-pool`.
425 : 49 : (define-private (cpmm-cost (selected-pool uint) (other-pool uint) (amount-shares uint))
426 : 130 : (begin
427 : : ;; Both pools must have liquidity
428 [ - ]: 130 : (asserts! (> selected-pool u0) err-insufficient-liquidity)
429 [ - ]: 130 : (asserts! (> other-pool u0) err-insufficient-liquidity)
430 : :
431 : : ;; You cannot buy so much that the counter-pool hits 0 or below MIN_POOL
432 : 130 : (let (
433 [ + - ]: 130 : (max-purchase (if (> other-pool MIN_POOL) (- other-pool MIN_POOL) u0))
434 : : )
435 [ - ]: 130 : (asserts! (<= amount-shares max-purchase) err-overbuy)
436 : :
437 : 130 : (let (
438 : 130 : (new-y (- other-pool amount-shares))
439 : 130 : (numerator (* selected-pool other-pool))
440 : 130 : (new-x (/ (* numerator SCALE) new-y))
441 : 130 : (cost (/ (- new-x (* selected-pool SCALE)) SCALE))
442 : : )
443 : 130 : (ok cost)
444 : : )
445 : : )
446 : : )
447 : : )
448 : :
449 : : ;; Read-only: get current price to buy `amount` shares in a category
450 : 2 : (define-read-only (get-max-shares (market-id uint) (index uint) (total-cost uint))
451 : 2 : (let (
452 : 2 : (fee-scaled (/ (* (* total-cost (var-get dev-fee-bips)) SCALE) u10000))
453 : 2 : (fee (/ fee-scaled SCALE))
454 [ + - ]: 2 : (cost-of-shares (if (> total-cost fee) (- total-cost fee) u0))
455 : 2 : (market-data (unwrap-panic (map-get? markets market-id)))
456 : 2 : (stake-list (get stakes market-data))
457 : 2 : (selected-pool (unwrap-panic (element-at? stake-list index)))
458 : 2 : (total-pool (fold + stake-list u0))
459 : 2 : (other-pool (- total-pool selected-pool))
460 [ + - ]: 2 : (max-by-floor (if (> other-pool MIN_POOL) (- other-pool MIN_POOL) u0))
461 : 2 : (shares (unwrap! (cpmm-shares selected-pool other-pool cost-of-shares) err-arithmetic))
462 [ - + ]: 2 : (shares-clamped (if (> shares max-by-floor) max-by-floor shares))
463 : : )
464 : 2 : (ok { shares: shares-clamped, fee: fee, cost-of-shares: cost-of-shares })
465 : : )
466 : : )
467 : : ;; Inverse: given a token `cost`, how many shares can be bought safely?
468 : 49 : (define-private (cpmm-shares (selected-pool uint) (other-pool uint) (cost uint))
469 : 128 : (begin
470 [ - ]: 128 : (asserts! (> selected-pool u0) err-insufficient-liquidity)
471 [ - ]: 128 : (asserts! (> other-pool u0) err-insufficient-liquidity)
472 : :
473 : 128 : (if (is-eq cost u0)
474 [ - ]: 0 : (ok u0)
475 [ + ]: 128 : (let (
476 : 128 : (denom (+ selected-pool cost)) ;; > selected-pool, non-zero
477 : 128 : (numerator (* selected-pool other-pool))
478 : 128 : (new-y (/ (* numerator SCALE) denom))
479 : 128 : (raw-shares (if (> (* other-pool SCALE) new-y)
480 [ + ]: 128 : (/ (- (* other-pool SCALE) new-y) SCALE)
481 [ - ]: 0 : u0))
482 : : ;; Enforce floor: clamp to keep MIN_POOL on the other side
483 [ + - ]: 128 : (max-by-floor (if (> other-pool MIN_POOL) (- other-pool MIN_POOL) u0))
484 [ - + ]: 128 : (shares (if (> raw-shares max-by-floor) max-by-floor raw-shares))
485 : : )
486 : 128 : (ok shares)
487 : : )
488 : : )
489 : : )
490 : : )
491 : : ;; Predict category with CPMM pricing
492 : 51 : (define-public (predict-category (market-id uint) (min-shares uint) (index uint) (token <ft-token>) (max-cost uint))
493 : 128 : (let (
494 : 128 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
495 : 128 : (categories (get categories md))
496 : 128 : (stake-tokens-list (get stake-tokens md))
497 : 128 : (selected-token-pool (unwrap! (element-at? stake-tokens-list index) err-category-not-found))
498 : 126 : (stake-list (get stakes md))
499 : 126 : (selected-pool (unwrap! (element-at? stake-list index) err-category-not-found))
500 : 126 : (total-pool (fold + stake-list u0))
501 : 126 : (other-pool (- total-pool selected-pool))
502 : 126 : (sender-balance (unwrap! (contract-call? token get-balance tx-sender) err-insufficient-balance))
503 : 126 : (fee (/ (* max-cost (var-get dev-fee-bips)) u10000))
504 [ + - ]: 126 : (cost-of-shares (if (> max-cost fee) (- max-cost fee) u0))
505 [ + - ]: 126 : (max-by-floor (if (> other-pool MIN_POOL) (- other-pool MIN_POOL) u0))
506 : 126 : (amount-shares (unwrap! (cpmm-shares selected-pool other-pool cost-of-shares) err-insufficient-balance))
507 : 126 : (max-cost-of-shares (unwrap! (cpmm-cost selected-pool other-pool max-by-floor) err-overbuy))
508 [ + - ]: 126 : (max-purchase (if (> other-pool u0) (- other-pool u1) u0))
509 : 126 : (market-end (+ (get market-start md) (get market-duration md)))
510 : : )
511 : : ;; Validate token and market state
512 [ - ]: 126 : (asserts! (< index (len categories)) err-category-not-found)
513 [ - ]: 126 : (asserts! (is-eq (get token md) (contract-of token)) err-invalid-token)
514 [ - ]: 126 : (asserts! (not (get concluded md)) err-market-not-concluded)
515 [ - ]: 126 : (asserts! (is-eq (get resolution-state md) RESOLUTION_OPEN) err-market-not-open)
516 [ - ]: 126 : (asserts! (>= max-cost u100) err-amount-too-low)
517 [ - ]: 126 : (asserts! (>= sender-balance max-cost) err-insufficient-balance)
518 [ - ]: 126 : (asserts! (<= max-cost u50000000000000) err-amount-too-high)
519 [ - ]: 126 : (asserts! (< burn-block-height market-end) err-market-ended)
520 : : ;; ensure the user cannot overpay for shares - this can skew liquidity in other pools
521 [ - ]: 126 : (asserts! (<= cost-of-shares max-cost-of-shares) err-overbuy)
522 [ - ]: 126 : (asserts! (< amount-shares other-pool) err-overbuy)
523 [ + ]: 126 : (asserts! (>= amount-shares min-shares) err-slippage-too-high)
524 [ - ]: 124 : (asserts! (> other-pool u0) err-insufficient-liquidity)
525 [ - ]: 124 : (asserts! (<= amount-shares max-by-floor) err-overbuy)
526 : :
527 : : ;; --- Token Transfers ---
528 : 124 : (try! (contract-call? token transfer cost-of-shares tx-sender (as-contract tx-sender) none))
529 : 124 : (if (> fee u0)
530 [ + ]: 116 : (try! (contract-call? token transfer fee tx-sender (var-get dev-fund) none))
531 [ + ]: 8 : true
532 : : )
533 : :
534 : : ;; --- Update Market State ---
535 : 122 : (let (
536 : 122 : (updated-stakes (unwrap! (replace-at? stake-list index (+ selected-pool amount-shares)) err-category-not-found))
537 : 122 : (updated-token-stakes (unwrap! (replace-at? stake-tokens-list index (+ selected-token-pool cost-of-shares)) err-category-not-found))
538 : : )
539 : 122 : (map-set markets market-id (merge md {stakes: updated-stakes, stake-tokens: updated-token-stakes}))
540 : : )
541 : :
542 : : ;; --- Update User Balances ---
543 : 122 : (let (
544 : 122 : (current-token-balances (default-to (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0) (map-get? token-balances {market-id: market-id, user: tx-sender})))
545 : 122 : (token-current (unwrap! (element-at? current-token-balances index) err-category-not-found))
546 : 122 : (user-token-updated (unwrap! (replace-at? current-token-balances index (+ token-current cost-of-shares)) err-category-not-found))
547 : :
548 : 122 : (current-stake-balances (default-to (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0) (map-get? stake-balances {market-id: market-id, user: tx-sender})))
549 : 122 : (user-current (unwrap! (element-at? current-stake-balances index) err-category-not-found))
550 : 122 : (user-stake-updated (unwrap! (replace-at? current-stake-balances index (+ user-current amount-shares)) err-category-not-found))
551 : : )
552 : 122 : (map-set stake-balances {market-id: market-id, user: tx-sender} user-stake-updated)
553 : 122 : (map-set token-balances {market-id: market-id, user: tx-sender} user-token-updated)
554 : 122 : (print {event: "market-stake", market-id: market-id, index: index, amount: amount-shares, cost: cost-of-shares, fee: fee, voter: tx-sender, max-cost: max-cost})
555 : 122 : (ok index)
556 : : )
557 : : )
558 : : )
559 : :
560 : : ;; Resolve a market invoked by ai-agent.
561 : 32 : (define-public (resolve-market (market-id uint))
562 : 52 : (let (
563 : 52 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
564 : 52 : (market-end (+ (get market-start md) (get market-duration md)))
565 : 52 : (market-close (+ market-end (get cool-down-period md)))
566 : 52 : (feed-id (get price-feed-id md))
567 : 52 : (start (get start-price md))
568 : 52 : (price-oracle (get-current-price-safe feed-id)) ;; uses v4 now
569 : :
570 : 0 : (price-final
571 : 52 : (unwrap!
572 : 52 : (match price-oracle
573 : : ;; OK branch from oracle
574 : : p
575 [ + ]: 52 : (let (
576 [ - + ]: 52 : (delta (if (> p start) (- p start) (- start p)))
577 [ + - ]: 52 : (move-bips (if (> start u0) (/ (* delta u10000) start) u10000))
578 : : )
579 : 52 : (if (<= move-bips (var-get max-move-bips))
580 [ + ]: 52 : (ok p) ;; accept oracle price
581 [ - ]: 0 : (get-manual-fallback market-id) ;; too volatile -> fallback
582 : : )
583 : : )
584 : : ;; ERR branch from oracle
585 : : e
586 [ - ]: 0 : (get-manual-fallback market-id) ;; oracle failed -> fallback
587 : : )
588 : 52 : err-oracle-no-fallback ;; unwrap! error if no manual fallback set
589 : : )
590 : : )
591 : :
592 : :
593 : 52 : (categories (get categories md))
594 : 52 : (first-category (unwrap! (element-at? categories u0) err-category-not-found))
595 : 0 : (winning-category-index
596 : 52 : (get winning-index
597 : 52 : (fold select-winner-pyth categories
598 : 52 : {current-index: u0, winning-index: none, price: price-final})))
599 : 0 : (final-index
600 : 52 : (if (is-some winning-category-index)
601 [ + ]: 52 : winning-category-index
602 [ - ]: 0 : (if (< price-final (get min first-category))
603 [ - ]: 0 : (some u0)
604 [ - ]: 0 : (some (- (len categories) u1)))))
605 : : )
606 [ - ][ + - ]: 52 : (asserts! (or (is-eq tx-sender (var-get resolution-agent)) (is-eq tx-sender (get creator md))) err-unauthorised)
607 [ + ]: 52 : (asserts! (>= burn-block-height market-close) err-market-wrong-state)
608 [ + ]: 36 : (asserts! (is-eq (get resolution-state md) RESOLUTION_OPEN) err-market-wrong-state)
609 [ - ]: 32 : (asserts! (is-some final-index) err-category-not-found)
610 : :
611 : 32 : (map-set markets market-id
612 : 32 : (merge md
613 : 32 : { outcome: final-index, price-outcome: (some price-final), resolution-state: RESOLUTION_RESOLVING, resolution-burn-height: burn-block-height }))
614 : 32 : (print {event: "resolve-market", market-id: market-id, outcome: final-index, price: price-final})
615 : 32 : (ok final-index)
616 : : )
617 : : )
618 : :
619 : 6 : (define-public (execute-hedge (market-id uint) (hedge-executor <hedge-trait>) (token0 <ft-velar-token>) (token1 <ft-velar-token>) (token-in <ft-velar-token>) (token-out <ft-velar-token>) )
620 : 6 : (let (
621 : 6 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
622 : 6 : (feed-id (get price-feed-id md))
623 : 6 : (hedged (get hedged md))
624 : 6 : (market-end (+ (get market-start md) (get market-duration md)))
625 : 6 : (stored-hedge-executor (default-to (var-get default-hedge-executor) (get hedge-executor md)))
626 : 6 : (predicted (get-biggest-pool-index (get stakes md)))
627 : : )
628 : : ;; check hedging allowed
629 [ + ]: 6 : (asserts! (var-get hedging-enabled) err-hedging-disabled)
630 : : ;; Ensure caller is the same contract that's stored
631 [ - ]: 5 : (asserts! (not hedged) err-already-hedged)
632 [ - ]: 5 : (asserts! (is-eq (contract-of hedge-executor) stored-hedge-executor) err-unauthorised)
633 : :
634 : : ;; Time window check
635 [ + ]: 5 : (asserts! (>= burn-block-height market-end) err-hedge-window)
636 [ + ]: 4 : (asserts! (< burn-block-height (+ market-end (get cool-down-period md))) err-hedge-window)
637 : :
638 : : ;; Compute crowd-predicted outcome
639 : 3 : (try! (contract-call? hedge-executor perform-swap-hedge market-id predicted feed-id token0 token1 token-in token-out))
640 : 0 : (map-set markets market-id (merge md {hedged: true}))
641 : 0 : (print {event: "hedge-action", market-id: market-id, predicted: predicted})
642 : 0 : (ok predicted)
643 : : )
644 : : )
645 : :
646 : 20 : (define-public (resolve-market-undisputed (market-id uint))
647 : 22 : (let (
648 : 22 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
649 : : )
650 [ + ]: 22 : (asserts! (> burn-block-height (+ (get resolution-burn-height md) (var-get dispute-window-length))) err-dispute-window-not-elapsed)
651 [ - ]: 16 : (asserts! (is-eq (get resolution-state md) RESOLUTION_RESOLVING) err-market-not-open)
652 : :
653 : 16 : (map-set markets market-id
654 : 16 : (merge md
655 : 16 : { concluded: true, resolution-state: RESOLUTION_RESOLVED, resolution-burn-height: burn-block-height }
656 : : )
657 : : )
658 : 16 : (print {event: "resolve-market-undisputed", market-id: market-id, resolution-burn-height: burn-block-height, resolution-state: RESOLUTION_RESOLVED})
659 : 16 : (ok true)
660 : : )
661 : : )
662 : :
663 : : ;; concludes a market that has been disputed. This method has to be called at least
664 : : ;; dispute-window-length blocks after the dispute was raised - the voting window.
665 : : ;; a proposal with 0 votes will close the market with the outcome false
666 : 4 : (define-public (resolve-market-vote (market-id uint) (outcome uint))
667 : 6 : (let (
668 : 6 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
669 : : )
670 : 6 : (try! (is-dao-or-extension))
671 [ - ]: 6 : (asserts! (< outcome (len (get categories md))) err-market-not-found)
672 [ + ][ + + ]: 6 : (asserts! (or (is-eq (get resolution-state md) RESOLUTION_DISPUTED) (is-eq (get resolution-state md) RESOLUTION_RESOLVING)) err-market-wrong-state)
673 : :
674 : 4 : (map-set markets market-id
675 : 4 : (merge md
676 : 4 : { concluded: true, outcome: (some outcome), resolution-state: RESOLUTION_RESOLVED }
677 : : )
678 : : )
679 : 4 : (print {event: "resolve-market-vote", market-id: market-id, resolver: contract-caller, outcome: outcome, resolution-state: RESOLUTION_RESOLVED})
680 : 4 : (ok true)
681 : : )
682 : : )
683 : :
684 : : ;; Allows a user with a stake in market to contest the resolution
685 : : ;; the call is made via the voting contract 'create-market-vote' function
686 : 4 : (define-public (dispute-resolution (market-id uint) (disputer principal) (num-categories uint))
687 : 4 : (let (
688 : 4 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
689 : : ;; ensure user has a stake
690 : 4 : (stake-data (unwrap! (map-get? stake-balances { market-id: market-id, user: disputer }) err-disputer-must-have-stake))
691 : : )
692 : : ;; user call create-market-vote in the voting contract to start a dispute
693 : 4 : (try! (is-dao-or-extension))
694 : :
695 [ - ]: 4 : (asserts! (is-eq num-categories (len (get categories md))) err-too-few-categories)
696 : : ;; prevent market getting locked in unresolved state
697 [ - ]: 4 : (asserts! (<= burn-block-height (+ (get resolution-burn-height md) (var-get dispute-window-length))) err-dispute-window-elapsed)
698 : :
699 [ - ]: 4 : (asserts! (is-eq (get resolution-state md) RESOLUTION_RESOLVING) err-market-not-resolving)
700 : :
701 : 4 : (map-set markets market-id
702 : 4 : (merge md { resolution-state: RESOLUTION_DISPUTED }))
703 : 4 : (print {event: "dispute-resolution", market-id: market-id, disputer: disputer, resolution-state: RESOLUTION_DISPUTED})
704 : 4 : (ok true)
705 : : )
706 : : )
707 : 4 : (define-public (force-resolve-market (market-id uint))
708 : 4 : (let (
709 : 4 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
710 : 4 : (elapsed (- burn-block-height (get resolution-burn-height md)))
711 : : )
712 : 4 : (begin
713 [ + ]: 4 : (asserts! (> elapsed (var-get resolution-timeout)) err-market-wrong-state)
714 [ - ]: 2 : (asserts! (is-eq (get resolution-state md) RESOLUTION_DISPUTED) err-market-wrong-state)
715 : :
716 : 2 : (map-set markets market-id
717 : 2 : (merge md { resolution-state: RESOLUTION_RESOLVED, concluded: true })
718 : : )
719 : 2 : (print {event: "force-resolve", market-id: market-id, resolution-state: RESOLUTION_RESOLVED})
720 : 2 : (ok true)
721 : : ))
722 : : )
723 : :
724 : : ;; Proportional payout with market fee only
725 : 18 : (define-public (claim-winnings (market-id uint) (token <ft-token>))
726 : 38 : (let (
727 : 38 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
728 : 38 : (index-won (unwrap! (get outcome md) err-market-not-concluded))
729 : 38 : (marketfee-bips (get market-fee-bips md))
730 : 38 : (treasury (get treasury md))
731 : 38 : (original-sender tx-sender)
732 : :
733 : : ;; user may have acquired shares via p2p and so have no entry under token-balances
734 : 38 : (user-token-list (default-to (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0) (map-get? token-balances {market-id: market-id, user: tx-sender})))
735 : 38 : (user-tokens (unwrap! (element-at? user-token-list index-won) err-user-not-staked))
736 : :
737 : 38 : (user-stake-list (unwrap! (map-get? stake-balances {market-id: market-id, user: tx-sender}) err-user-not-staked))
738 : 36 : (user-shares (unwrap! (element-at? user-stake-list index-won) err-user-not-staked))
739 : :
740 : 36 : (stake-list (get stakes md))
741 : 36 : (winning-pool (unwrap! (element-at? stake-list index-won) err-market-not-concluded))
742 : 36 : (total-share-pool (fold + stake-list u0))
743 : :
744 : 36 : (staked-tokens (get stake-tokens md))
745 : 36 : (total-token-pool (fold + staked-tokens u0))
746 : :
747 : : ;; CPMM Payout: the proportion of the total tokens staked to the shares won
748 : 36 : (gross-refund-scaled (if (> winning-pool u0)
749 [ + ]: 36 : (/ (* (* user-shares total-token-pool) SCALE) winning-pool)
750 [ - ]: 0 : u0))
751 : 36 : (gross-refund (/ gross-refund-scaled SCALE))
752 : :
753 : 36 : (marketfee (/ (* gross-refund marketfee-bips) u10000))
754 : 36 : (net-refund (- gross-refund marketfee))
755 : : )
756 : : ;; Check resolved and non zero payout
757 [ - ]: 36 : (asserts! (is-eq (get resolution-state md) RESOLUTION_RESOLVED) err-market-not-concluded)
758 [ - ]: 36 : (asserts! (get concluded md) err-market-not-concluded)
759 [ + ]: 36 : (asserts! (> user-shares u0) err-user-not-winner-or-claimed)
760 [ - ]: 18 : (asserts! (> winning-pool u0) err-amount-too-low)
761 [ - ]: 18 : (asserts! (> net-refund u0) err-user-share-is-zero)
762 : :
763 : : ;; Transfer winnings and market fee
764 : 18 : (as-contract
765 : 18 : (begin
766 : 18 : (if (> marketfee u0)
767 [ - ]: 0 : (try! (contract-call? token transfer marketfee tx-sender treasury none))
768 [ + ]: 18 : true
769 : : )
770 : 18 : (try! (contract-call? token transfer net-refund tx-sender original-sender none))
771 : : )
772 : : )
773 : :
774 : : ;; Zero out stake
775 : 18 : (map-set token-balances {market-id: market-id, user: tx-sender} (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0))
776 : 18 : (map-set stake-balances {market-id: market-id, user: tx-sender} (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0))
777 : 18 : (try! (contract-call? .bme030-0-reputation-token mint tx-sender u1 u10))
778 : 18 : (print {event: "claim-winnings", market-id: market-id, index-won: index-won, claimer: tx-sender, user-tokens: user-tokens, user-shares: user-shares, refund: net-refund, marketfee: marketfee, winning-pool: winning-pool, total-pool: total-share-pool})
779 : 18 : (ok net-refund)
780 : : )
781 : : )
782 : :
783 : 4 : (define-read-only (get-expected-payout (market-id uint) (index uint) (user principal))
784 : 6 : (let (
785 : 6 : (md (unwrap-panic (map-get? markets market-id)))
786 : :
787 : 6 : (token-pool (fold + (get stake-tokens md) u0))
788 : :
789 : 6 : (user-shares-list (unwrap-panic (map-get? stake-balances {market-id: market-id, user: user})))
790 : 6 : (user-shares (unwrap-panic (element-at? user-shares-list index)))
791 : :
792 : 6 : (winning-shares-pool (unwrap-panic (element-at? (get stakes md) index)))
793 : :
794 : 6 : (marketfee-bips (get market-fee-bips md))
795 [ + - ]: 6 : (gross-refund (if (> winning-shares-pool u0) (/ (* user-shares token-pool) winning-shares-pool) u0))
796 : 6 : (marketfee (/ (* gross-refund marketfee-bips) u10000))
797 : 6 : (net-refund (- gross-refund marketfee))
798 : : )
799 [ + + + ]: 6 : (if (and (> user-shares u0) (> winning-shares-pool u0) (> net-refund u0))
800 [ + ]: 2 : (ok { net-refund: net-refund, marketfee: marketfee-bips })
801 [ + ]: 4 : (err u1) ;; not eligible or payout = 0
802 : : )
803 : : )
804 : : )
805 : :
806 : : ;; marketplace transfer function to move shares - dao extension callable
807 : : ;; note - an automated dao function that fulfils orders functions as a 'sell-shares' feature
808 : 2 : (define-public (transfer-shares
809 : : (market-id uint)
810 : : (outcome uint)
811 : : (seller principal)
812 : : (buyer principal)
813 : : (amount uint)
814 : : (token <ft-token>)
815 : : )
816 : 2 : (let (
817 : 2 : (md (unwrap! (map-get? markets market-id) err-market-not-found))
818 : 2 : (stake-list (get stakes md))
819 : 2 : (market-token (get token md))
820 : 2 : (selected-pool (unwrap! (element-at? stake-list outcome) err-category-not-found))
821 : 2 : (other-pools (- (fold + stake-list u0) selected-pool))
822 : :
823 : : ;; Pricing
824 : 2 : (price (unwrap! (cpmm-cost selected-pool other-pools amount) err-overbuy))
825 : 2 : (marketfee-bips (get market-fee-bips md))
826 : 2 : (treasury (get treasury md))
827 : 2 : (fee (/ (* price marketfee-bips) u10000))
828 : 2 : (net-price (- price fee))
829 : 2 : (reduced-fee (/ fee u2))
830 : :
831 : : ;; Share balances
832 : 2 : (seller-balances (unwrap! (map-get? stake-balances {market-id: market-id, user: seller}) err-user-not-staked))
833 : 2 : (seller-shares (unwrap! (element-at? seller-balances outcome) err-user-not-staked))
834 : 2 : (buyer-balances (default-to (list u0 u0 u0 u0 u0 u0 u0 u0 u0 u0) (map-get? stake-balances {market-id: market-id, user: buyer})))
835 : 2 : (buyer-shares (unwrap! (element-at? buyer-balances outcome) err-category-not-found))
836 : : )
837 : : ;; dao extension callable only
838 : 2 : (try! (is-dao-or-extension))
839 : : ;; Ensure seller has enough shares
840 [ - ]: 2 : (asserts! (>= seller-shares amount) err-user-share-is-zero)
841 [ - ]: 2 : (asserts! (is-eq market-token (contract-of token)) err-invalid-token)
842 [ - ]: 2 : (asserts! (is-eq (get resolution-state md) RESOLUTION_OPEN) err-market-wrong-state)
843 : :
844 : : ;; Perform share transfer
845 : : ;; Note: we do not update `stakes` here because total pool liquidity remains unchanged.
846 : 2 : (let (
847 : 2 : (buyer-updated (unwrap! (replace-at? buyer-balances outcome (+ buyer-shares amount)) err-category-not-found))
848 : 2 : (seller-updated (unwrap! (replace-at? seller-balances outcome (- seller-shares amount)) err-category-not-found))
849 : : )
850 : : ;; Update state
851 : 2 : (map-set stake-balances {market-id: market-id, user: buyer} buyer-updated)
852 : 2 : (map-set stake-balances {market-id: market-id, user: seller} seller-updated)
853 : :
854 : : ;; Transfer cost and fee from buyer to seller
855 : 2 : (begin
856 : 2 : (if (> reduced-fee u0)
857 : : ;; buyer pays reduced fee as p2p incentive
858 [ - ]: 0 : (try! (contract-call? token transfer reduced-fee buyer treasury none))
859 [ + ]: 2 : true
860 : : )
861 : 2 : (try! (contract-call? token transfer net-price buyer seller none))
862 : : )
863 : 2 : (print {event: "transfer-shares", market-id: market-id, outcome: outcome, buyer: buyer, seller: seller, amount: amount, price: net-price, fee: fee })
864 : 2 : (ok price)
865 : : )
866 : : )
867 : : )
868 : :
869 : : ;; Helper function to create a list with zeros after index N
870 : 57 : (define-private (zero-after-n (original-list (list 10 uint)) (n uint))
871 : 65 : (let (
872 [ + - ]: 65 : (element-0 (if (<= u0 n) (unwrap-panic (element-at? original-list u0)) u0))
873 [ + - ]: 65 : (element-1 (if (< u1 n) (unwrap-panic (element-at? original-list u1)) u0))
874 [ + - ]: 65 : (element-2 (if (< u2 n) (unwrap-panic (element-at? original-list u2)) u0))
875 [ + - ]: 65 : (element-3 (if (< u3 n) (unwrap-panic (element-at? original-list u3)) u0))
876 [ + - ]: 65 : (element-4 (if (< u4 n) (unwrap-panic (element-at? original-list u4)) u0))
877 [ + - ]: 65 : (element-5 (if (< u5 n) (unwrap-panic (element-at? original-list u5)) u0))
878 [ - + ]: 65 : (element-6 (if (< u6 n) (unwrap-panic (element-at? original-list u6)) u0))
879 [ - + ]: 65 : (element-7 (if (< u7 n) (unwrap-panic (element-at? original-list u7)) u0))
880 [ - + ]: 65 : (element-8 (if (< u8 n) (unwrap-panic (element-at? original-list u8)) u0))
881 [ - + ]: 65 : (element-9 (if (< u9 n) (unwrap-panic (element-at? original-list u9)) u0))
882 : : )
883 : 65 : (list element-0 element-1 element-2 element-3 element-4 element-5 element-6 element-7 element-8 element-9)
884 : : )
885 : : )
886 : :
887 : 6 : (define-private (get-biggest-pool-index (lst (list 10 uint)))
888 : 6 : (get index
889 : 6 : (fold find-max-helper
890 : 6 : lst
891 : 6 : { max-val: u0, index: u0, current-index: u0 }))
892 : : )
893 : :
894 : 6 : (define-private (find-max-helper (val uint) (state { max-val: uint, index: uint, current-index: uint }))
895 : : {
896 [ + + ]: 60 : max-val: (if (> val (get max-val state)) val (get max-val state)),
897 [ + + ]: 60 : index: (if (> val (get max-val state)) (get current-index state) (get index state)),
898 : 60 : current-index: (+ (get current-index state) u1)
899 : : }
900 : : )
901 : :
902 : 57 : (define-private (category-bands (start-price uint) (delta uint))
903 : 65 : (let (
904 : : ;; Symmetric bands around start-price
905 : 65 : (element-0 {min: u0, max: (- start-price (* delta u2))}) ;; big loss
906 : 65 : (element-1 {min: (- start-price (* delta u2)), max: (- start-price delta)}) ;; small loss
907 : 65 : (element-2 {min: (- start-price delta), max: start-price}) ;; slight loss
908 : 65 : (element-3 {min: start-price, max: (+ start-price delta)}) ;; slight gain
909 : 65 : (element-4 {min: (+ start-price delta), max: (+ start-price (* delta u2))}) ;; small gain
910 : 65 : (element-5 {min: (+ start-price (* delta u2)), max: u4294967295}) ;; big gain
911 : : )
912 : 65 : (list element-0 element-1 element-2 element-3 element-4 element-5)
913 : : )
914 : : )
915 : :
916 : 57 : (define-private (get-current-price-safe (feed-id (buff 32)))
917 : 119 : (let (
918 : 119 : (d (unwrap! (contract-call? .pyth-oracle-v4 get-price feed-id .pyth-storage-v4) err-oracle-call-failed))
919 : 119 : (raw-price (to-uint (abs-int (get price d))))
920 : 119 : (raw-conf (get conf d)) ;; uint
921 : 119 : (expo (get expo d)) ;; int
922 : 119 : (ts (get publish-time d)) ;; uint (not optional)
923 : 119 : (price (scale-pyth raw-price expo)) ;; -> uint
924 : 119 : (conf (scale-pyth raw-conf expo)) ;; -> uint
925 [ + + ]: 119 : (conf-bips (if (> price u0) (/ (* conf u10000) price) u10000))
926 : 119 : (now (now-seconds))
927 : : )
928 : 119 : (begin
929 [ + ]: 119 : (asserts! (> price u0) err-oracle-zero-price)
930 [ - ]: 117 : (asserts! (< (abs-int expo) 20) err-oracle-expo)
931 : : ;; confidence bound
932 [ - ]: 117 : (asserts! (<= conf-bips (var-get max-conf-bips)) err-oracle-uncertain)
933 : : ;; freshness check in seconds
934 : : ;;(asserts! (<= (- now ts) (var-get max-staleness-secs)) err-oracle-stale)
935 : 117 : (ok price)
936 : : )
937 : : )
938 : : )
939 : :
940 : 32 : (define-private (select-winner-pyth
941 : : (category (tuple (min uint) (max uint)))
942 : : (acc {current-index: uint, winning-index: (optional uint), price: uint}))
943 : 312 : (let (
944 : 312 : (price (get price acc))
945 : 312 : (min-price (get min category))
946 : 312 : (max-price (get max category))
947 : 312 : (current-index (get current-index acc))
948 : : )
949 : : ;; Check if the price falls within this category's range
950 [ + + + ]: 312 : (if (and (>= price min-price) (< price max-price) (is-none (get winning-index acc)))
951 : 52 : {current-index: (+ current-index u1), winning-index: (some current-index), price: price}
952 : 260 : {current-index: (+ current-index u1), winning-index: (get winning-index acc), price: price}
953 : : )
954 : : )
955 : : )
956 : :
957 : 26 : (define-public (set-manual-price (market-id uint) (price uint))
958 : 78 : (begin
959 : 78 : (try! (is-dao-or-extension))
960 : 78 : (map-set manual-fallback-price market-id price)
961 : 78 : (ok true)
962 : : )
963 : : )
964 : :
965 : : ;; Pyth v4 and audit updates
966 : :
967 : 57 : (define-read-only (abs-int (x int))
968 [ + + ]: 712 : (if (< x 0) (- 0 x) x)
969 : : )
970 : :
971 : 57 : (define-private (scale-pyth (val uint) (expo int)) ;; -> uint
972 : 238 : (let ((v-abs (abs-int (to-int val))))
973 : 238 : (if (< expo 0)
974 : : ;; multiply for negative exponent
975 [ + ]: 238 : (to-uint (* v-abs (to-int (pow u10 (to-uint (abs-int expo))))))
976 : : ;; divide for positive exponent
977 [ - ]: 0 : (to-uint (/ v-abs (to-int (pow u10 (to-uint expo))))))
978 : : )
979 : : )
980 : :
981 : 57 : (define-read-only (now-seconds)
982 : 119 : (unwrap-panic (get-stacks-block-info? time (- stacks-block-height u1)))
983 : : )
984 : :
985 : 0 : (define-private (get-manual-fallback (market-id uint))
986 : 0 : (match (map-get? manual-fallback-price market-id)
987 [ - ]: 0 : price (ok price)
988 [ - ]: 0 : (err err-oracle-no-fallback)
989 : : )
990 : : )
991 : :
992 : :
993 : : ;; --- Extension callback
994 : :
995 : 1 : (define-public (callback (sender principal) (memo (buff 34)))
996 : 1 : (ok true)
997 : : )
|