LCOV - code coverage report
Current view: top level - contracts/extensions - bme024-0-market-scalar-pyth.clar (source / functions) Coverage Total Hit
Test: lcov.info Lines: 95.4 % 519 495
Test Date: 2025-11-10 13:03:30 Functions: 94.0 % 50 47
Branches: 43.8 % 160 70

             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                 :             : )
        

Generated by: LCOV version 2.3.2-1