diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a2ac06..c514fea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,31 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Format Clarity contracts + run: clarinet format --in-place + - name: Check Clarity contracts check run: clarinet check --use-on-disk-deployment-plan + + fuzz-tests: + runs-on: ubuntu-latest + container: ghcr.io/stx-labs/clarinet:latest + steps: + - uses: actions/checkout@v4 + + - name: Install Node.js in container + run: | + apt-get update && apt-get install -y curl + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y nodejs + + - name: Install dependencies + run: npm ci + + - name: Run fuzz tests + run: npx rv . roxy test + + tests: diff --git a/contracts/roxy.clar b/contracts/roxy.clar index 3d9d811..0bbc16b 100644 --- a/contracts/roxy.clar +++ b/contracts/roxy.clar @@ -56,76 +56,149 @@ ;; data maps ;; User Point System -(define-map user-points principal uint) -(define-map earned-points principal uint) ;; Points earned from predictions (used for selling threshold) -(define-map user-names principal (string-ascii 50)) ;; User names -(define-map usernames (string-ascii 50) principal) ;; Track username for uniqueness (username -> user) +(define-map user-points + principal + uint +) +(define-map earned-points + principal + uint +) +;; Points earned from predictions (used for selling threshold) +(define-map user-names + principal + (string-ascii 50) +) +;; User names +(define-map usernames + (string-ascii 50) + principal +) +;; Track username for uniqueness (username -> user) ;; Prediction Event Registry -(define-map events uint { +(define-map events + uint + { yes-pool: uint, no-pool: uint, status: (string-ascii 20), ;; "open", "closed", "resolved" winner: (optional bool), creator: principal, - metadata: (string-ascii 200) -}) + metadata: (string-ascii 200), + } +) ;; User Staking System -(define-map yes-stakes (tuple (event-id uint) (user principal)) uint) -(define-map no-stakes (tuple (event-id uint) (user principal)) uint) +(define-map yes-stakes + { + event-id: uint, + user: principal, + } + uint +) +(define-map no-stakes + { + event-id: uint, + user: principal, + } + uint +) ;; Point Marketplace -(define-map listings uint { +(define-map listings + uint + { seller: principal, points: uint, price-stx: uint, - active: bool -}) + active: bool, + } +) ;; Transaction Log (for event tracking - Clarity doesn't have native events) ;; Maps: (block-height, tx-index) -> transaction log entry (define-data-var next-log-id uint u1) -(define-map transaction-logs uint { - action: (string-ascii 30), ;; "register", "create-event", "stake-yes", "stake-no", "resolve", "claim", "create-listing", "buy-listing", "cancel-listing" +(define-map transaction-logs + uint + { + action: (string-ascii 30), ;; "register", "create-event", "stake-yes", "stake-no", "resolve", "claim", "create-listing", "buy-listing", "cancel-listing" user: principal, event-id: (optional uint), listing-id: (optional uint), amount: (optional uint), - metadata: (string-ascii 200) -}) + metadata: (string-ascii 200), + } +) ;; Guild System (Collaborative Predictions) (define-data-var next-guild-id uint u1) -(define-map guilds uint { +(define-map guilds + uint + { creator: principal, name: (string-ascii 50), total-points: uint, - member-count: uint -}) -(define-map guild-members (tuple (guild-id uint) (user principal)) bool) ;; Is user a member of guild -(define-map guild-deposits (tuple (guild-id uint) (user principal)) uint) ;; User's contribution to guild pool -(define-map guild-yes-stakes (tuple (guild-id uint) (event-id uint)) uint) ;; Guild's YES stake on event -(define-map guild-no-stakes (tuple (guild-id uint) (event-id uint)) uint) ;; Guild's NO stake on event + member-count: uint, + } +) +(define-map guild-members + { + guild-id: uint, + user: principal, + } + bool +) +;; Is user a member of guild +(define-map guild-deposits + { + guild-id: uint, + user: principal, + } + uint +) +;; User's contribution to guild pool +(define-map guild-yes-stakes + { + guild-id: uint, + event-id: uint, + } + uint +) +;; Guild's YES stake on event +(define-map guild-no-stakes + { + guild-id: uint, + event-id: uint, + } + uint +) +;; Guild's NO stake on event ;; Leaderboard System ;; User Leaderboard Statistics -(define-map user-stats principal { +(define-map user-stats + principal + { total-predictions: uint, wins: uint, losses: uint, total-points-earned: uint, - win-rate: uint ;; Stored as percentage (0-10000, where 10000 = 100%) -}) + win-rate: uint, ;; Stored as percentage (0-10000, where 10000 = 100%) + } +) ;; Guild Leaderboard Statistics -(define-map guild-stats uint { +(define-map guild-stats + uint + { total-predictions: uint, wins: uint, losses: uint, total-points-earned: uint, - win-rate: uint ;; Stored as percentage (0-10000, where 10000 = 100%) -}) + win-rate: uint, ;; Stored as percentage (0-10000, where 10000 = 100%) + } +) ;; public functions @@ -133,7 +206,7 @@ ;; 1. register (username) ;; ============================================================================ ;; Purpose: Register a new user in the system. -;; +;; ;; Details: ;; - Takes a username (up to 50 ASCII characters) ;; - Checks if the user is already registered (error u1 if yes) @@ -154,43 +227,45 @@ ;; - ERR-USERNAME-TAKEN if username is already taken by another user ;; ============================================================================ (define-public (register (username (string-ascii 50))) - (let ((user tx-sender)) - (match (map-get? user-points user) - existing ERR-USER-ALREADY-REGISTERED ;; User already registered - (begin - ;; Track username for uniqueness - (match (map-get? usernames username) - existing-user ERR-USERNAME-TAKEN ;; Username already taken - (begin - (map-set user-points user STARTING_POINTS) - (map-set earned-points user u0) ;; Starting points don't count as earned - (map-set user-names user username) - (map-set usernames username user) ;; Store username -> user mapping for uniqueness - ;; Emit event - (print { - event: "user-registered", - user: user, - username: username, - points: STARTING_POINTS - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "register", - user: user, - event-id: none, - listing-id: none, - amount: (some STARTING_POINTS), - metadata: username - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok true) - ) - ) + (let ((user tx-sender)) + (match (map-get? user-points user) + existing + ERR-USER-ALREADY-REGISTERED ;; User already registered + (begin + ;; Track username for uniqueness + (match (map-get? usernames username) + existing-user + ERR-USERNAME-TAKEN ;; Username already taken + (begin + (map-set user-points user STARTING_POINTS) + (map-set earned-points user u0) ;; Starting points don't count as earned + (map-set user-names user username) + (map-set usernames username user) ;; Store username -> user mapping for uniqueness + ;; Emit event + (print { + event: "user-registered", + user: user, + username: username, + points: STARTING_POINTS, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "register", + user: user, + event-id: none, + listing-id: none, + amount: (some STARTING_POINTS), + metadata: username, + }) + (var-set next-log-id (+ log-id u1)) ) + (ok true) + ) ) + ) ) + ) ) ;; ============================================================================ @@ -220,43 +295,48 @@ ;; - ERR-NOT-ADMIN if caller is not admin ;; - ERR-EVENT-ID-EXISTS if event ID already exists ;; ============================================================================ -(define-public (create-event (event-id uint) (metadata (string-ascii 200))) - (let ((caller tx-sender)) - (asserts! (is-eq caller (var-get admin)) ERR-NOT-ADMIN) ;; Only admin can create events - (match (map-get? events event-id) - existing ERR-EVENT-ID-EXISTS ;; Event ID already exists - (begin - (map-set events event-id { - yes-pool: u0, - no-pool: u0, - status: "open", - winner: none, - creator: caller, - metadata: metadata - }) - ;; Emit event - (print { - event: "event-created", - event-id: event-id, - creator: caller, - metadata: metadata - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "create-event", - user: caller, - event-id: (some event-id), - listing-id: none, - amount: none, - metadata: metadata - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok true) - ) +(define-public (create-event + (event-id uint) + (metadata (string-ascii 200)) + ) + (let ((caller tx-sender)) + (asserts! (is-eq caller (var-get admin)) ERR-NOT-ADMIN) + ;; Only admin can create events + (match (map-get? events event-id) + existing + ERR-EVENT-ID-EXISTS ;; Event ID already exists + (begin + (map-set events event-id { + yes-pool: u0, + no-pool: u0, + status: "open", + winner: none, + creator: caller, + metadata: metadata, + }) + ;; Emit event + (print { + event: "event-created", + event-id: event-id, + creator: caller, + metadata: metadata, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "create-event", + user: caller, + event-id: (some event-id), + listing-id: none, + amount: none, + metadata: metadata, + }) + (var-set next-log-id (+ log-id u1)) ) + (ok true) + ) ) + ) ) ;; ============================================================================ @@ -287,84 +367,105 @@ ;; - ERR-USER-NOT-REGISTERED if user not registered ;; - ERR-EVENT-NOT-FOUND if event not found ;; ============================================================================ -(define-public (stake-yes (event-id uint) (amount uint)) - (let ((user tx-sender)) - (asserts! (> amount u0) ERR-INVALID-AMOUNT) ;; Amount must be greater than 0 - (match (map-get? events event-id) - event (begin - (asserts! (is-eq (get status event) "open") ERR-EVENT-NOT-OPEN) ;; Event must be open - (match (map-get? user-points user) - current-points (begin - (asserts! (>= current-points amount) ERR-INSUFFICIENT-POINTS) ;; Insufficient points - ;; Deduct points from user - (map-set user-points user (- current-points amount)) - ;; Add to YES pool - (let ((new-yes-pool (+ (get yes-pool event) amount)) - (new-no-pool (get no-pool event))) - (map-set events event-id { - yes-pool: new-yes-pool, - no-pool: new-no-pool, - status: (get status event), - winner: (get winner event), - creator: (get creator event), - metadata: (get metadata event) - }) - ;; Record user's stake - (match (map-get? yes-stakes (tuple (event-id event-id) (user user))) - existing-stake (map-set yes-stakes (tuple (event-id event-id) (user user)) (+ existing-stake amount)) - (begin - (map-set yes-stakes (tuple (event-id event-id) (user user)) amount) - ;; Track new prediction for leaderboard - (match (map-get? user-stats user) - stats (map-set user-stats user { - total-predictions: (+ (get total-predictions stats) u1), - wins: (get wins stats), - losses: (get losses stats), - total-points-earned: (get total-points-earned stats), - win-rate: (get win-rate stats) - }) - (map-set user-stats user { - total-predictions: u1, - wins: u0, - losses: u0, - total-points-earned: u0, - win-rate: u0 - }) - ) - ) - ) - ;; Update total YES stakes - (var-set total-yes-stakes (+ (var-get total-yes-stakes) amount)) - ;; Emit event - (print { - event: "staked-yes", - event-id: event-id, - user: user, - amount: amount, - yes-pool: new-yes-pool, - no-pool: new-no-pool - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "stake-yes", - user: user, - event-id: (some event-id), - listing-id: none, - amount: (some amount), - metadata: "stake-yes" - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok true) - ) - ) - ERR-USER-NOT-REGISTERED ;; User not registered - ) +(define-public (stake-yes + (event-id uint) + (amount uint) + ) + (let ((user tx-sender)) + (asserts! (> amount u0) ERR-INVALID-AMOUNT) + ;; Amount must be greater than 0 + (match (map-get? events event-id) + event + (begin + (asserts! (is-eq (get status event) "open") ERR-EVENT-NOT-OPEN) ;; Event must be open + (match (map-get? user-points user) + current-points + (begin + (asserts! (>= current-points amount) ERR-INSUFFICIENT-POINTS) ;; Insufficient points + ;; Deduct points from user + (map-set user-points user (- current-points amount)) + ;; Add to YES pool + (let ( + (new-yes-pool (+ (get yes-pool event) amount)) + (new-no-pool (get no-pool event)) + ) + (map-set events event-id { + yes-pool: new-yes-pool, + no-pool: new-no-pool, + status: (get status event), + winner: (get winner event), + creator: (get creator event), + metadata: (get metadata event), + }) + ;; Record user's stake + (match (map-get? yes-stakes { + event-id: event-id, + user: user, + }) + existing-stake (map-set yes-stakes { + event-id: event-id, + user: user, + } + (+ existing-stake amount) + ) + (begin + (map-set yes-stakes { + event-id: event-id, + user: user, + } + amount + ) + ;; Track new prediction for leaderboard + (match (map-get? user-stats user) + stats (map-set user-stats user { + total-predictions: (+ (get total-predictions stats) u1), + wins: (get wins stats), + losses: (get losses stats), + total-points-earned: (get total-points-earned stats), + win-rate: (get win-rate stats), + }) + (map-set user-stats user { + total-predictions: u1, + wins: u0, + losses: u0, + total-points-earned: u0, + win-rate: u0, + }) + ) + ) + ) + ;; Update total YES stakes + (var-set total-yes-stakes (+ (var-get total-yes-stakes) amount)) + ;; Emit event + (print { + event: "staked-yes", + event-id: event-id, + user: user, + amount: amount, + yes-pool: new-yes-pool, + no-pool: new-no-pool, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "stake-yes", + user: user, + event-id: (some event-id), + listing-id: none, + amount: (some amount), + metadata: "stake-yes", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok true) ) - ERR-EVENT-NOT-FOUND ;; Event not found + ) + ERR-USER-NOT-REGISTERED ;; User not registered ) + ) + ERR-EVENT-NOT-FOUND ;; Event not found ) + ) ) ;; ============================================================================ @@ -392,84 +493,105 @@ ;; - ERR-USER-NOT-REGISTERED if user not registered ;; - ERR-EVENT-NOT-FOUND if event not found ;; ============================================================================ -(define-public (stake-no (event-id uint) (amount uint)) - (let ((user tx-sender)) - (asserts! (> amount u0) ERR-INVALID-AMOUNT) ;; Amount must be greater than 0 - (match (map-get? events event-id) - event (begin - (asserts! (is-eq (get status event) "open") ERR-EVENT-NOT-OPEN) ;; Event must be open - (match (map-get? user-points user) - current-points (begin - (asserts! (>= current-points amount) ERR-INSUFFICIENT-POINTS) ;; Insufficient points - ;; Deduct points from user - (map-set user-points user (- current-points amount)) - ;; Add to NO pool - (let ((new-yes-pool (get yes-pool event)) - (new-no-pool (+ (get no-pool event) amount))) - (map-set events event-id { - yes-pool: new-yes-pool, - no-pool: new-no-pool, - status: (get status event), - winner: (get winner event), - creator: (get creator event), - metadata: (get metadata event) - }) - ;; Record user's stake - (match (map-get? no-stakes (tuple (event-id event-id) (user user))) - existing-stake (map-set no-stakes (tuple (event-id event-id) (user user)) (+ existing-stake amount)) - (begin - (map-set no-stakes (tuple (event-id event-id) (user user)) amount) - ;; Track new prediction for leaderboard - (match (map-get? user-stats user) - stats (map-set user-stats user { - total-predictions: (+ (get total-predictions stats) u1), - wins: (get wins stats), - losses: (get losses stats), - total-points-earned: (get total-points-earned stats), - win-rate: (get win-rate stats) - }) - (map-set user-stats user { - total-predictions: u1, - wins: u0, - losses: u0, - total-points-earned: u0, - win-rate: u0 - }) - ) - ) - ) - ;; Update total NO stakes - (var-set total-no-stakes (+ (var-get total-no-stakes) amount)) - ;; Emit event - (print { - event: "staked-no", - event-id: event-id, - user: user, - amount: amount, - yes-pool: new-yes-pool, - no-pool: new-no-pool - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "stake-no", - user: user, - event-id: (some event-id), - listing-id: none, - amount: (some amount), - metadata: "stake-yes" - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok true) - ) - ) - ERR-USER-NOT-REGISTERED ;; User not registered - ) +(define-public (stake-no + (event-id uint) + (amount uint) + ) + (let ((user tx-sender)) + (asserts! (> amount u0) ERR-INVALID-AMOUNT) + ;; Amount must be greater than 0 + (match (map-get? events event-id) + event + (begin + (asserts! (is-eq (get status event) "open") ERR-EVENT-NOT-OPEN) ;; Event must be open + (match (map-get? user-points user) + current-points + (begin + (asserts! (>= current-points amount) ERR-INSUFFICIENT-POINTS) ;; Insufficient points + ;; Deduct points from user + (map-set user-points user (- current-points amount)) + ;; Add to NO pool + (let ( + (new-yes-pool (get yes-pool event)) + (new-no-pool (+ (get no-pool event) amount)) + ) + (map-set events event-id { + yes-pool: new-yes-pool, + no-pool: new-no-pool, + status: (get status event), + winner: (get winner event), + creator: (get creator event), + metadata: (get metadata event), + }) + ;; Record user's stake + (match (map-get? no-stakes { + event-id: event-id, + user: user, + }) + existing-stake (map-set no-stakes { + event-id: event-id, + user: user, + } + (+ existing-stake amount) + ) + (begin + (map-set no-stakes { + event-id: event-id, + user: user, + } + amount + ) + ;; Track new prediction for leaderboard + (match (map-get? user-stats user) + stats (map-set user-stats user { + total-predictions: (+ (get total-predictions stats) u1), + wins: (get wins stats), + losses: (get losses stats), + total-points-earned: (get total-points-earned stats), + win-rate: (get win-rate stats), + }) + (map-set user-stats user { + total-predictions: u1, + wins: u0, + losses: u0, + total-points-earned: u0, + win-rate: u0, + }) + ) + ) + ) + ;; Update total NO stakes + (var-set total-no-stakes (+ (var-get total-no-stakes) amount)) + ;; Emit event + (print { + event: "staked-no", + event-id: event-id, + user: user, + amount: amount, + yes-pool: new-yes-pool, + no-pool: new-no-pool, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "stake-no", + user: user, + event-id: (some event-id), + listing-id: none, + amount: (some amount), + metadata: "stake-yes", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok true) ) - ERR-EVENT-NOT-FOUND ;; Event not found + ) + ERR-USER-NOT-REGISTERED ;; User not registered ) + ) + ERR-EVENT-NOT-FOUND ;; Event not found ) + ) ) ;; ============================================================================ @@ -496,41 +618,56 @@ ;; - ERR-EVENT-NOT-FOUND if event not found ;; - ERR-EVENT-MUST-BE-OPEN if event must be open to resolve ;; ============================================================================ -(define-public (resolve-event (event-id uint) (winner bool)) - (let ((caller tx-sender)) - (asserts! (is-eq caller (var-get admin)) ERR-NOT-ADMIN) ;; Only admin can resolve - (match (map-get? events event-id) - event (begin - (asserts! (is-eq (get status event) "open") ERR-EVENT-MUST-BE-OPEN) ;; Event must be open to resolve - (let ((yes-pool (get yes-pool event)) - (no-pool (get no-pool event))) - (map-set events event-id { - yes-pool: yes-pool, - no-pool: no-pool, - status: "resolved", - winner: (some winner), - creator: (get creator event), - metadata: (get metadata event) - }) - ;; Log transaction - (let ((log-id (var-get next-log-id)) - (winner-str (if winner "yes" "no"))) - (map-set transaction-logs log-id { - action: "resolve", - user: caller, - event-id: (some event-id), - listing-id: none, - amount: none, - metadata: (if winner "winner-yes" "winner-no") - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok true) - ) +(define-public (resolve-event + (event-id uint) + (winner bool) + ) + (let ((caller tx-sender)) + (asserts! (is-eq caller (var-get admin)) ERR-NOT-ADMIN) + ;; Only admin can resolve + (match (map-get? events event-id) + event + (begin + (asserts! (is-eq (get status event) "open") ERR-EVENT-MUST-BE-OPEN) ;; Event must be open to resolve + (let ( + (yes-pool (get yes-pool event)) + (no-pool (get no-pool event)) + ) + (map-set events event-id { + yes-pool: yes-pool, + no-pool: no-pool, + status: "resolved", + winner: (some winner), + creator: (get creator event), + metadata: (get metadata event), + }) + ;; Log transaction + (let ( + (log-id (var-get next-log-id)) + (winner-str (if winner + "yes" + "no" + )) ) - ERR-EVENT-NOT-FOUND ;; Event not found + (map-set transaction-logs log-id { + action: "resolve", + user: caller, + event-id: (some event-id), + listing-id: none, + amount: none, + metadata: (if winner + "winner-yes" + "winner-no" + ), + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok true) ) + ) + ERR-EVENT-NOT-FOUND ;; Event not found ) + ) ) ;; ============================================================================ @@ -572,416 +709,550 @@ ;; - ERR-WINNER-NOT-SET if winner not set ;; ============================================================================ (define-public (claim (event-id uint)) - (let ((user tx-sender)) - (match (map-get? events event-id) - event (begin - (asserts! (is-eq (get status event) "resolved") ERR-EVENT-MUST-BE-RESOLVED) ;; Event must be resolved - (match (get winner event) - winner (begin - (let ((yes-pool (get yes-pool event)) - (no-pool (get no-pool event)) - (total-pool (+ yes-pool no-pool)) - (winning-pool (if winner yes-pool no-pool))) - (if (is-eq winning-pool u0) - ERR-NO-WINNERS ;; No winners (pool is empty) - (begin - (if winner - ;; User staked YES - (match (map-get? yes-stakes (tuple (event-id event-id) (user user))) - stake (begin - (if (> stake u0) - (let ((reward (/ (* stake total-pool) winning-pool))) - ;; Add reward to user points - (match (map-get? user-points user) - current-points (begin - (let ((new-total-points (+ current-points reward))) - (map-set user-points user new-total-points) - ;; Update earned points for selling threshold - (match (map-get? earned-points user) - current-earned (begin - (let ((new-earned-points (+ current-earned reward))) - (map-set earned-points user new-earned-points) - ;; Update total YES stakes (subtract before clearing) - (var-set total-yes-stakes (- (var-get total-yes-stakes) stake)) - ;; Clear the stake - (map-set yes-stakes (tuple (event-id event-id) (user user)) u0) - ;; Update leaderboard stats (WIN) - (match (map-get? user-stats user) - stats (begin - (let ((new-wins (+ (get wins stats) u1)) - (new-total-earned (+ (get total-points-earned stats) reward)) - (new-win-rate (/ (* new-wins u10000) (+ new-wins (get losses stats))))) - (map-set user-stats user { - total-predictions: (get total-predictions stats), - wins: new-wins, - losses: (get losses stats), - total-points-earned: new-total-earned, - win-rate: new-win-rate - }) - ) - ) - (map-set user-stats user { - total-predictions: u1, - wins: u1, - losses: u0, - total-points-earned: reward, - win-rate: u10000 - }) - ) - ;; Emit event - (print { - event: "reward-claimed", - event-id: event-id, - user: user, - reward: reward, - total-points: new-total-points, - earned-points: new-earned-points - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "claim", - user: user, - event-id: (some event-id), - listing-id: none, - amount: (some reward), - metadata: "reward-claimed" - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok reward) - ) - ) - (begin - (map-set earned-points user reward) - ;; Update total YES stakes (subtract before clearing) - (var-set total-yes-stakes (- (var-get total-yes-stakes) stake)) - ;; Clear the stake - (map-set yes-stakes (tuple (event-id event-id) (user user)) u0) - ;; Update leaderboard stats (WIN) - (match (map-get? user-stats user) - stats (begin - (let ((new-wins (+ (get wins stats) u1)) - (new-total-earned (+ (get total-points-earned stats) reward)) - (new-win-rate (/ (* new-wins u10000) (+ new-wins (get losses stats))))) - (map-set user-stats user { - total-predictions: (get total-predictions stats), - wins: new-wins, - losses: (get losses stats), - total-points-earned: new-total-earned, - win-rate: new-win-rate - }) - ) - ) - (map-set user-stats user { - total-predictions: u1, - wins: u1, - losses: u0, - total-points-earned: reward, - win-rate: u10000 - }) - ) - ;; Emit event - (print { - event: "reward-claimed", - event-id: event-id, - user: user, - reward: reward, - total-points: new-total-points, - earned-points: reward - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "claim", - user: user, - event-id: (some event-id), - listing-id: none, - amount: (some reward), - metadata: "reward-claimed" - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok reward) - ) - ) - ) - ) - ERR-USER-NOT-REGISTERED ;; User not registered - ) - ) - (begin - ;; Check if user had NO stake (they lost) - (match (map-get? no-stakes (tuple (event-id event-id) (user user))) - no-stake (begin - (if (> no-stake u0) - (begin - ;; User had NO stake but YES won - clear stake and track loss - ;; Update total NO stakes (subtract before clearing) - (var-set total-no-stakes (- (var-get total-no-stakes) no-stake)) - (map-set no-stakes (tuple (event-id event-id) (user user)) u0) - ;; Update leaderboard stats (LOSS) - (match (map-get? user-stats user) - stats (begin - (let ((new-losses (+ (get losses stats) u1)) - (total-games (+ (get wins stats) new-losses)) - (new-win-rate (if (is-eq total-games u0) u0 (/ (* (get wins stats) u10000) total-games)))) - (map-set user-stats user { - total-predictions: (get total-predictions stats), - wins: (get wins stats), - losses: new-losses, - total-points-earned: (get total-points-earned stats), - win-rate: new-win-rate - }) - ) - ) - (map-set user-stats user { - total-predictions: u1, - wins: u0, - losses: u1, - total-points-earned: u0, - win-rate: u0 - }) - ) - ;; Return success with 0 reward to indicate loss tracked (state changes persist) - (ok u0) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ;; User staked NO - (let ((no-stake-opt (map-get? no-stakes (tuple (event-id event-id) (user user))))) - (if (is-some no-stake-opt) - (let ((stake (unwrap! no-stake-opt ERR-NO-STAKE-FOUND))) - (if (> stake u0) - (let ((reward (/ (* stake total-pool) winning-pool))) - ;; Add reward to user points - (match (map-get? user-points user) - current-points (begin - (let ((new-total-points (+ current-points reward))) - (map-set user-points user new-total-points) - ;; Update earned points for selling threshold - (match (map-get? earned-points user) - current-earned (begin - (let ((new-earned-points (+ current-earned reward))) - (map-set earned-points user new-earned-points) - ;; Update total NO stakes (subtract before clearing) - (var-set total-no-stakes (- (var-get total-no-stakes) stake)) - ;; Clear the stake - (map-set no-stakes (tuple (event-id event-id) (user user)) u0) - ;; Update leaderboard stats (WIN) - (match (map-get? user-stats user) - stats (begin - (let ((new-wins (+ (get wins stats) u1)) - (new-total-earned (+ (get total-points-earned stats) reward)) - (new-win-rate (/ (* new-wins u10000) (+ new-wins (get losses stats))))) - (map-set user-stats user { - total-predictions: (get total-predictions stats), - wins: new-wins, - losses: (get losses stats), - total-points-earned: new-total-earned, - win-rate: new-win-rate - }) - ) - ) - (map-set user-stats user { - total-predictions: u1, - wins: u1, - losses: u0, - total-points-earned: reward, - win-rate: u10000 - }) - ) - ;; Emit event - (print { - event: "reward-claimed", - event-id: event-id, - user: user, - reward: reward, - total-points: new-total-points, - earned-points: new-earned-points - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "claim", - user: user, - event-id: (some event-id), - listing-id: none, - amount: (some reward), - metadata: "reward-claimed" - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok reward) - ) - ) - (begin - (map-set earned-points user reward) - ;; Update total NO stakes (subtract before clearing) - (var-set total-no-stakes (- (var-get total-no-stakes) stake)) - ;; Clear the stake - (map-set no-stakes (tuple (event-id event-id) (user user)) u0) - ;; Update leaderboard stats (WIN) - (match (map-get? user-stats user) - stats (begin - (let ((new-wins (+ (get wins stats) u1)) - (new-total-earned (+ (get total-points-earned stats) reward)) - (new-win-rate (/ (* new-wins u10000) (+ new-wins (get losses stats))))) - (map-set user-stats user { - total-predictions: (get total-predictions stats), - wins: new-wins, - losses: (get losses stats), - total-points-earned: new-total-earned, - win-rate: new-win-rate - }) - ) - ) - (map-set user-stats user { - total-predictions: u1, - wins: u1, - losses: u0, - total-points-earned: reward, - win-rate: u10000 - }) - ) - ;; Emit event - (print { - event: "reward-claimed", - event-id: event-id, - user: user, - reward: reward, - total-points: new-total-points, - earned-points: reward - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "claim", - user: user, - event-id: (some event-id), - listing-id: none, - amount: (some reward), - metadata: "reward-claimed" - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok reward) - ) - ) - ) - ) - ERR-USER-NOT-REGISTERED ;; User not registered - ) - ) - (begin - ;; User had NO stake but it's 0, check if user had YES stake (they lost) - (match (map-get? yes-stakes (tuple (event-id event-id) (user user))) - yes-stake (begin - (if (> yes-stake u0) - (begin - ;; User had YES stake but NO won - clear stake and track loss - ;; Update total YES stakes (subtract before clearing) - (var-set total-yes-stakes (- (var-get total-yes-stakes) yes-stake)) - (map-set yes-stakes (tuple (event-id event-id) (user user)) u0) - ;; Update leaderboard stats (LOSS) - (match (map-get? user-stats user) - stats (begin - (let ((new-losses (+ (get losses stats) u1)) - (total-games (+ (get wins stats) new-losses)) - (new-win-rate (if (is-eq total-games u0) u0 (/ (* (get wins stats) u10000) total-games)))) - (map-set user-stats user { - total-predictions: (get total-predictions stats), - wins: (get wins stats), - losses: new-losses, - total-points-earned: (get total-points-earned stats), - win-rate: new-win-rate - }) - ) - ) - (map-set user-stats user { - total-predictions: u1, - wins: u0, - losses: u1, - total-points-earned: u0, - win-rate: u0 - }) - ) - ;; Return success with 0 reward to indicate loss tracked (state changes persist) - (ok u0) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ) - ) - (begin - ;; No NO stake found, check if user had YES stake (they lost) - (let ((yes-stake-opt (map-get? yes-stakes (tuple (event-id event-id) (user user))))) - (if (is-some yes-stake-opt) - (let ((yes-stake (unwrap! yes-stake-opt ERR-NO-STAKE-FOUND))) - (if (> yes-stake u0) - (begin - ;; User had YES stake but NO won - clear stake and track loss - ;; Update total YES stakes (subtract before clearing) - (var-set total-yes-stakes (- (var-get total-yes-stakes) yes-stake)) - (map-set yes-stakes (tuple (event-id event-id) (user user)) u0) - ;; Update leaderboard stats (LOSS) - (match (map-get? user-stats user) - stats (begin - (let ((new-losses (+ (get losses stats) u1)) - (total-games (+ (get wins stats) new-losses)) - (new-win-rate (if (is-eq total-games u0) u0 (/ (* (get wins stats) u10000) total-games)))) - (map-set user-stats user { - total-predictions: (get total-predictions stats), - wins: (get wins stats), - losses: new-losses, - total-points-earned: (get total-points-earned stats), - win-rate: new-win-rate - }) - ) - ) - (map-set user-stats user { - total-predictions: u1, - wins: u0, - losses: u1, - total-points-earned: u0, - win-rate: u0 - }) - ) - ;; Return success with 0 reward to indicate loss tracked (state changes persist) - (ok u0) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ) + (let ((user tx-sender)) + (match (map-get? events event-id) + event + (begin + (asserts! (is-eq (get status event) "resolved") + ERR-EVENT-MUST-BE-RESOLVED + ) + ;; Event must be resolved + (match (get winner event) + winner + (begin + (let ( + (yes-pool (get yes-pool event)) + (no-pool (get no-pool event)) + (total-pool (+ yes-pool no-pool)) + (winning-pool (if winner + yes-pool + no-pool + )) + ) + (if (is-eq winning-pool u0) + ERR-NO-WINNERS ;; No winners (pool is empty) + (begin + (if winner + ;; User staked YES + (match (map-get? yes-stakes { + event-id: event-id, + user: user, + }) + stake + (begin + (if (> stake u0) + (let ((reward (/ (* stake total-pool) winning-pool))) + ;; Add reward to user points + (match (map-get? user-points user) + current-points + (begin + (let ((new-total-points (+ current-points reward))) + (map-set user-points user new-total-points) + ;; Update earned points for selling threshold + (match (map-get? earned-points user) + current-earned (begin + (let ((new-earned-points (+ current-earned reward))) + (map-set earned-points user + new-earned-points + ) + ;; Update total YES stakes (subtract before clearing) + (var-set total-yes-stakes + (- (var-get total-yes-stakes) stake) + ) + ;; Clear the stake + (map-set yes-stakes { + event-id: event-id, + user: user, + } + u0 + ) + ;; Update leaderboard stats (WIN) + (match (map-get? user-stats user) + stats (begin + (let ( + (new-wins (+ (get wins stats) u1)) + (new-total-earned (+ + (get total-points-earned stats) + reward + )) + (new-win-rate (/ (* new-wins u10000) + (+ new-wins (get losses stats)) + )) + ) + (map-set user-stats user { + total-predictions: (get total-predictions stats), + wins: new-wins, + losses: (get losses stats), + total-points-earned: new-total-earned, + win-rate: new-win-rate, + }) + ) + ) + (map-set user-stats user { + total-predictions: u1, + wins: u1, + losses: u0, + total-points-earned: reward, + win-rate: u10000, + }) + ) + ;; Emit event + (print { + event: "reward-claimed", + event-id: event-id, + user: user, + reward: reward, + total-points: new-total-points, + earned-points: new-earned-points, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "claim", + user: user, + event-id: (some event-id), + listing-id: none, + amount: (some reward), + metadata: "reward-claimed", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok reward) + ) + ) + (begin + (map-set earned-points user reward) + ;; Update total YES stakes (subtract before clearing) + (var-set total-yes-stakes + (- (var-get total-yes-stakes) stake) + ) + ;; Clear the stake + (map-set yes-stakes { + event-id: event-id, + user: user, + } + u0 + ) + ;; Update leaderboard stats (WIN) + (match (map-get? user-stats user) + stats (begin + (let ( + (new-wins (+ (get wins stats) u1)) + (new-total-earned (+ (get total-points-earned stats) + reward + )) + (new-win-rate (/ (* new-wins u10000) + (+ new-wins (get losses stats)) + )) + ) + (map-set user-stats user { + total-predictions: (get total-predictions stats), + wins: new-wins, + losses: (get losses stats), + total-points-earned: new-total-earned, + win-rate: new-win-rate, + }) + ) + ) + (map-set user-stats user { + total-predictions: u1, + wins: u1, + losses: u0, + total-points-earned: reward, + win-rate: u10000, + }) + ) + ;; Emit event + (print { + event: "reward-claimed", + event-id: event-id, + user: user, + reward: reward, + total-points: new-total-points, + earned-points: reward, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "claim", + user: user, + event-id: (some event-id), + listing-id: none, + amount: (some reward), + metadata: "reward-claimed", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok reward) + ) + ) + ) + ) + ERR-USER-NOT-REGISTERED ;; User not registered + ) + ) + (begin + ;; Check if user had NO stake (they lost) + (match (map-get? no-stakes { + event-id: event-id, + user: user, + }) + no-stake + (begin + (if (> no-stake u0) + (begin + ;; User had NO stake but YES won - clear stake and track loss + ;; Update total NO stakes (subtract before clearing) + (var-set total-no-stakes + (- (var-get total-no-stakes) no-stake) + ) + (map-set no-stakes { + event-id: event-id, + user: user, + } + u0 + ) + ;; Update leaderboard stats (LOSS) + (match (map-get? user-stats user) + stats (begin + (let ( + (new-losses (+ (get losses stats) u1)) + (total-games (+ (get wins stats) new-losses)) + (new-win-rate (if (is-eq total-games u0) + u0 + (/ (* (get wins stats) u10000) + total-games + ) + )) + ) + (map-set user-stats user { + total-predictions: (get total-predictions stats), + wins: (get wins stats), + losses: new-losses, + total-points-earned: (get total-points-earned stats), + win-rate: new-win-rate, + }) + ) + ) + (map-set user-stats user { + total-predictions: u1, + wins: u0, + losses: u1, + total-points-earned: u0, + win-rate: u0, + }) + ) + ;; Return success with 0 reward to indicate loss tracked (state changes persist) + (ok u0) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ;; User staked NO + (let ((no-stake-opt (map-get? no-stakes { + event-id: event-id, + user: user, + }))) + (if (is-some no-stake-opt) + (let ((stake (unwrap! no-stake-opt ERR-NO-STAKE-FOUND))) + (if (> stake u0) + (let ((reward (/ (* stake total-pool) winning-pool))) + ;; Add reward to user points + (match (map-get? user-points user) + current-points + (begin + (let ((new-total-points (+ current-points reward))) + (map-set user-points user new-total-points) + ;; Update earned points for selling threshold + (match (map-get? earned-points user) + current-earned (begin + (let ((new-earned-points (+ current-earned reward))) + (map-set earned-points user + new-earned-points + ) + ;; Update total NO stakes (subtract before clearing) + (var-set total-no-stakes + (- (var-get total-no-stakes) stake) + ) + ;; Clear the stake + (map-set no-stakes { + event-id: event-id, + user: user, + } + u0 + ) + ;; Update leaderboard stats (WIN) + (match (map-get? user-stats user) + stats (begin + (let ( + (new-wins (+ (get wins stats) u1)) + (new-total-earned (+ + (get total-points-earned + stats ) + reward + )) + (new-win-rate (/ (* new-wins u10000) + (+ new-wins + (get losses stats) + ))) ) + (map-set user-stats user { + total-predictions: (get total-predictions stats), + wins: new-wins, + losses: (get losses stats), + total-points-earned: new-total-earned, + win-rate: new-win-rate, + }) + ) ) + (map-set user-stats user { + total-predictions: u1, + wins: u1, + losses: u0, + total-points-earned: reward, + win-rate: u10000, + }) + ) + ;; Emit event + (print { + event: "reward-claimed", + event-id: event-id, + user: user, + reward: reward, + total-points: new-total-points, + earned-points: new-earned-points, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "claim", + user: user, + event-id: (some event-id), + listing-id: none, + amount: (some reward), + metadata: "reward-claimed", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok reward) ) + ) + (begin + (map-set earned-points user reward) + ;; Update total NO stakes (subtract before clearing) + (var-set total-no-stakes + (- (var-get total-no-stakes) stake) + ) + ;; Clear the stake + (map-set no-stakes { + event-id: event-id, + user: user, + } + u0 + ) + ;; Update leaderboard stats (WIN) + (match (map-get? user-stats user) + stats (begin + (let ( + (new-wins (+ (get wins stats) u1)) + (new-total-earned (+ + (get total-points-earned stats) + reward + )) + (new-win-rate (/ (* new-wins u10000) + (+ new-wins (get losses stats)) + )) + ) + (map-set user-stats user { + total-predictions: (get total-predictions stats), + wins: new-wins, + losses: (get losses stats), + total-points-earned: new-total-earned, + win-rate: new-win-rate, + }) + ) + ) + (map-set user-stats user { + total-predictions: u1, + wins: u1, + losses: u0, + total-points-earned: reward, + win-rate: u10000, + }) + ) + ;; Emit event + (print { + event: "reward-claimed", + event-id: event-id, + user: user, + reward: reward, + total-points: new-total-points, + earned-points: reward, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "claim", + user: user, + event-id: (some event-id), + listing-id: none, + amount: (some reward), + metadata: "reward-claimed", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok reward) + ) + ) + ) + ) + ERR-USER-NOT-REGISTERED ;; User not registered + ) + ) + (begin + ;; User had NO stake but it's 0, check if user had YES stake (they lost) + (match (map-get? yes-stakes { + event-id: event-id, + user: user, + }) + yes-stake + (begin + (if (> yes-stake u0) + (begin + ;; User had YES stake but NO won - clear stake and track loss + ;; Update total YES stakes (subtract before clearing) + (var-set total-yes-stakes + (- (var-get total-yes-stakes) yes-stake) + ) + (map-set yes-stakes { + event-id: event-id, + user: user, + } + u0 + ) + ;; Update leaderboard stats (LOSS) + (match (map-get? user-stats user) + stats (begin + (let ( + (new-losses (+ (get losses stats) u1)) + (total-games (+ (get wins stats) new-losses)) + (new-win-rate (if (is-eq total-games u0) + u0 + (/ (* (get wins stats) u10000) + total-games + ) + )) + ) + (map-set user-stats user { + total-predictions: (get total-predictions stats), + wins: (get wins stats), + losses: new-losses, + total-points-earned: (get total-points-earned stats), + win-rate: new-win-rate, + }) + ) + ) + (map-set user-stats user { + total-predictions: u1, + wins: u0, + losses: u1, + total-points-earned: u0, + win-rate: u0, + }) + ) + ;; Return success with 0 reward to indicate loss tracked (state changes persist) + (ok u0) ) + ERR-NO-STAKE-FOUND ;; No stake found + ) ) + ERR-NO-STAKE-FOUND ;; No stake found + ) ) - ERR-WINNER-NOT-SET ;; Winner not set + ) + ) + (begin + ;; No NO stake found, check if user had YES stake (they lost) + (let ((yes-stake-opt (map-get? yes-stakes { + event-id: event-id, + user: user, + }))) + (if (is-some yes-stake-opt) + (let ((yes-stake (unwrap! yes-stake-opt ERR-NO-STAKE-FOUND))) + (if (> yes-stake u0) + (begin + ;; User had YES stake but NO won - clear stake and track loss + ;; Update total YES stakes (subtract before clearing) + (var-set total-yes-stakes + (- (var-get total-yes-stakes) yes-stake) + ) + (map-set yes-stakes { + event-id: event-id, + user: user, + } + u0 + ) + ;; Update leaderboard stats (LOSS) + (match (map-get? user-stats user) + stats (begin + (let ( + (new-losses (+ (get losses stats) u1)) + (total-games (+ (get wins stats) new-losses)) + (new-win-rate (if (is-eq total-games u0) + u0 + (/ (* (get wins stats) u10000) + total-games + ) + )) + ) + (map-set user-stats user { + total-predictions: (get total-predictions stats), + wins: (get wins stats), + losses: new-losses, + total-points-earned: (get total-points-earned stats), + win-rate: new-win-rate, + }) + ) + ) + (map-set user-stats user { + total-predictions: u1, + wins: u0, + losses: u1, + total-points-earned: u0, + win-rate: u0, + }) + ) + ;; Return success with 0 reward to indicate loss tracked (state changes persist) + (ok u0) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ) + ) ) + ) + ) + ) ) - ERR-EVENT-NOT-FOUND ;; Event not found + ) + ERR-WINNER-NOT-SET ;; Winner not set ) + ) + ERR-EVENT-NOT-FOUND ;; Event not found ) + ) ) ;; ============================================================================ @@ -1016,61 +1287,70 @@ ;; - ERR-USER-NOT-REGISTERED if user not registered ;; - ERR-INSUFFICIENT-EARNED-POINTS if must have earned >= 10,000 points ;; ============================================================================ -(define-public (create-listing (points uint) (price-stx uint)) - (let ((seller tx-sender)) - (asserts! (> points u0) ERR-INVALID-AMOUNT) ;; Points must be greater than 0 - (asserts! (> price-stx u0) ERR-INVALID-AMOUNT) ;; Price must be greater than 0 - ;; Check if user can sell (earned-points >= 10,000) - (match (map-get? earned-points seller) - earned (begin - (asserts! (>= earned MIN_EARNED_FOR_SELL) ERR-INSUFFICIENT-EARNED-POINTS) ;; Must have earned at least 10,000 points - (match (map-get? user-points seller) - current-points (begin - (asserts! (>= current-points points) ERR-INSUFFICIENT-POINTS) ;; Insufficient points - ;; Transfer STX listing fee to contract - (try! (stx-transfer? LISTING_FEE seller (as-contract tx-sender))) - ;; Add listing fee to protocol treasury - (var-set protocol-treasury (+ (var-get protocol-treasury) LISTING_FEE)) - ;; Lock seller's points by deducting them - (map-set user-points seller (- current-points points)) - ;; Create listing - (let ((listing-id (var-get next-listing-id))) - (map-set listings listing-id { - seller: seller, - points: points, - price-stx: price-stx, - active: true - }) - (var-set next-listing-id (+ listing-id u1)) - ;; Emit event - (print { - event: "listing-created", - listing-id: listing-id, - seller: seller, - points: points, - price-stx: price-stx - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "create-listing", - user: seller, - event-id: none, - listing-id: (some listing-id), - amount: (some points), - metadata: "listing-created" - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok listing-id) - ) - ) - ERR-USER-NOT-REGISTERED ;; User not registered - ) +(define-public (create-listing + (points uint) + (price-stx uint) + ) + (let ((seller tx-sender)) + (asserts! (> points u0) ERR-INVALID-AMOUNT) + ;; Points must be greater than 0 + (asserts! (> price-stx u0) ERR-INVALID-AMOUNT) + ;; Price must be greater than 0 + ;; Check if user can sell (earned-points >= 10,000) + (match (map-get? earned-points seller) + earned + (begin + (asserts! (>= earned MIN_EARNED_FOR_SELL) ERR-INSUFFICIENT-EARNED-POINTS) ;; Must have earned at least 10,000 points + (match (map-get? user-points seller) + current-points + (begin + (asserts! (>= current-points points) ERR-INSUFFICIENT-POINTS) ;; Insufficient points + ;; Transfer STX listing fee to contract + (try! (stx-transfer? LISTING_FEE seller (as-contract tx-sender))) + ;; Add listing fee to protocol treasury + (var-set protocol-treasury + (+ (var-get protocol-treasury) LISTING_FEE) ) - ERR-INSUFFICIENT-EARNED-POINTS ;; Must have earned at least 10,000 points + ;; Lock seller's points by deducting them + (map-set user-points seller (- current-points points)) + ;; Create listing + (let ((listing-id (var-get next-listing-id))) + (map-set listings listing-id { + seller: seller, + points: points, + price-stx: price-stx, + active: true, + }) + (var-set next-listing-id (+ listing-id u1)) + ;; Emit event + (print { + event: "listing-created", + listing-id: listing-id, + seller: seller, + points: points, + price-stx: price-stx, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "create-listing", + user: seller, + event-id: none, + listing-id: (some listing-id), + amount: (some points), + metadata: "listing-created", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok listing-id) + ) + ) + ERR-USER-NOT-REGISTERED ;; User not registered ) + ) + ERR-INSUFFICIENT-EARNED-POINTS ;; Must have earned at least 10,000 points ) + ) ) ;; ============================================================================ @@ -1110,79 +1390,94 @@ ;; - ERR-LISTING-NOT-FOUND if listing not found ;; - ERR-INSUFFICIENT-AVAILABLE-POINTS if points-to-buy > available points ;; ============================================================================ -(define-public (buy-listing (listing-id uint) (points-to-buy uint)) - (let ((buyer tx-sender)) - (asserts! (> points-to-buy u0) ERR-INVALID-AMOUNT) ;; Points to buy must be greater than 0 - (match (map-get? listings listing-id) - listing (begin - (asserts! (get active listing) ERR-LISTING-NOT-ACTIVE) ;; Listing must be active - (let ((seller (get seller listing)) - (total-points (get points listing)) - (total-price-stx (get price-stx listing))) - (asserts! (>= total-points points-to-buy) ERR-INSUFFICIENT-AVAILABLE-POINTS) ;; Not enough points available - ;; Calculate proportional price - (let ((price-per-point (/ total-price-stx total-points)) - (actual-price-stx (* price-per-point points-to-buy)) - (protocol-fee (/ (* actual-price-stx PROTOCOL_FEE_BPS) BPS_DENOMINATOR)) - (seller-amount (- actual-price-stx protocol-fee)) - (remaining-points (- total-points points-to-buy)) - (remaining-price-stx (- total-price-stx actual-price-stx))) - ;; Transfer STX from buyer to seller (98%) - (try! (stx-transfer? seller-amount buyer seller)) - ;; Transfer protocol fee (2%) to contract treasury - (try! (stx-transfer? protocol-fee buyer (as-contract tx-sender))) - (var-set protocol-treasury (+ (var-get protocol-treasury) protocol-fee)) - ;; Transfer points to buyer - (match (map-get? user-points buyer) - buyer-points (map-set user-points buyer (+ buyer-points points-to-buy)) - (map-set user-points buyer points-to-buy) ;; Buyer not registered, but we'll give them points anyway - ) - ;; Emit event - (print { - event: "listing-bought", - listing-id: listing-id, - buyer: buyer, - seller: seller, - points: points-to-buy, - price-stx: actual-price-stx, - protocol-fee: protocol-fee - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "buy-listing", - user: buyer, - event-id: none, - listing-id: (some listing-id), - amount: (some points-to-buy), - metadata: "listing-bought" - }) - (var-set next-log-id (+ log-id u1)) - ) - ;; Update listing: partial purchase keeps it active, full purchase deactivates - (if (is-eq remaining-points u0) - ;; Full purchase - deactivate listing - (map-set listings listing-id { - seller: seller, - points: u0, - price-stx: u0, - active: false - }) - ;; Partial purchase - update listing with remaining points and price - (map-set listings listing-id { - seller: seller, - points: remaining-points, - price-stx: remaining-price-stx, - active: true - }) - ) - (ok true) - ) - ) +(define-public (buy-listing + (listing-id uint) + (points-to-buy uint) + ) + (let ((buyer tx-sender)) + (asserts! (> points-to-buy u0) ERR-INVALID-AMOUNT) + ;; Points to buy must be greater than 0 + (match (map-get? listings listing-id) + listing + (begin + (asserts! (get active listing) ERR-LISTING-NOT-ACTIVE) ;; Listing must be active + (let ( + (seller (get seller listing)) + (total-points (get points listing)) + (total-price-stx (get price-stx listing)) + ) + (asserts! (>= total-points points-to-buy) + ERR-INSUFFICIENT-AVAILABLE-POINTS + ) + ;; Not enough points available + ;; Calculate proportional price + (let ( + (price-per-point (/ total-price-stx total-points)) + (actual-price-stx (* price-per-point points-to-buy)) + (protocol-fee (/ (* actual-price-stx PROTOCOL_FEE_BPS) BPS_DENOMINATOR)) + (seller-amount (- actual-price-stx protocol-fee)) + (remaining-points (- total-points points-to-buy)) + (remaining-price-stx (- total-price-stx actual-price-stx)) + ) + ;; Transfer STX from buyer to seller (98%) + (try! (stx-transfer? seller-amount buyer seller)) + ;; Transfer protocol fee (2%) to contract treasury + (try! (stx-transfer? protocol-fee buyer (as-contract tx-sender))) + (var-set protocol-treasury + (+ (var-get protocol-treasury) protocol-fee) + ) + ;; Transfer points to buyer + (match (map-get? user-points buyer) + buyer-points + (map-set user-points buyer (+ buyer-points points-to-buy)) + (map-set user-points buyer points-to-buy) ;; Buyer not registered, but we'll give them points anyway + ) + ;; Emit event + (print { + event: "listing-bought", + listing-id: listing-id, + buyer: buyer, + seller: seller, + points: points-to-buy, + price-stx: actual-price-stx, + protocol-fee: protocol-fee, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "buy-listing", + user: buyer, + event-id: none, + listing-id: (some listing-id), + amount: (some points-to-buy), + metadata: "listing-bought", + }) + (var-set next-log-id (+ log-id u1)) + ) + ;; Update listing: partial purchase keeps it active, full purchase deactivates + (if (is-eq remaining-points u0) + ;; Full purchase - deactivate listing + (map-set listings listing-id { + seller: seller, + points: u0, + price-stx: u0, + active: false, + }) + ;; Partial purchase - update listing with remaining points and price + (map-set listings listing-id { + seller: seller, + points: remaining-points, + price-stx: remaining-price-stx, + active: true, + }) ) - ERR-LISTING-NOT-FOUND ;; Listing not found + (ok true) + ) ) + ) + ERR-LISTING-NOT-FOUND ;; Listing not found ) + ) ) ;; ============================================================================ @@ -1210,49 +1505,50 @@ ;; - ERR-ONLY-SELLER-CAN-CANCEL if only seller can cancel ;; ============================================================================ (define-public (cancel-listing (listing-id uint)) - (let ((caller tx-sender)) - (match (map-get? listings listing-id) - listing (begin - (asserts! (is-eq caller (get seller listing)) ERR-ONLY-SELLER-CAN-CANCEL) ;; Only seller can cancel - (asserts! (get active listing) ERR-LISTING-NOT-ACTIVE) ;; Listing must be active - (let ((points (get points listing))) - ;; Return points to seller - (match (map-get? user-points caller) - seller-points (map-set user-points caller (+ seller-points points)) - (map-set user-points caller points) - ) - ;; Emit event - (print { - event: "listing-cancelled", - listing-id: listing-id, - seller: caller, - points: points - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "cancel-listing", - user: caller, - event-id: none, - listing-id: (some listing-id), - amount: (some points), - metadata: "cancelled" - }) - (var-set next-log-id (+ log-id u1)) - ) - ;; Deactivate listing - (map-set listings listing-id { - seller: caller, - points: points, - price-stx: (get price-stx listing), - active: false - }) - (ok true) - ) - ) - ERR-LISTING-NOT-FOUND ;; Listing not found + (let ((caller tx-sender)) + (match (map-get? listings listing-id) + listing + (begin + (asserts! (is-eq caller (get seller listing)) ERR-ONLY-SELLER-CAN-CANCEL) ;; Only seller can cancel + (asserts! (get active listing) ERR-LISTING-NOT-ACTIVE) ;; Listing must be active + (let ((points (get points listing))) + ;; Return points to seller + (match (map-get? user-points caller) + seller-points (map-set user-points caller (+ seller-points points)) + (map-set user-points caller points) + ) + ;; Emit event + (print { + event: "listing-cancelled", + listing-id: listing-id, + seller: caller, + points: points, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "cancel-listing", + user: caller, + event-id: none, + listing-id: (some listing-id), + amount: (some points), + metadata: "cancelled", + }) + (var-set next-log-id (+ log-id u1)) + ) + ;; Deactivate listing + (map-set listings listing-id { + seller: caller, + points: points, + price-stx: (get price-stx listing), + active: false, + }) + (ok true) ) + ) + ERR-LISTING-NOT-FOUND ;; Listing not found ) + ) ) ;; ============================================================================ @@ -1280,38 +1576,43 @@ ;; - ERR-INSUFFICIENT-TREASURY if treasury balance is insufficient ;; ============================================================================ (define-public (withdraw-protocol-fees (amount uint)) - (let ((caller tx-sender) - (admin-principal (var-get admin))) - (asserts! (is-eq caller admin-principal) ERR-NOT-ADMIN) ;; Only admin can withdraw - (asserts! (> amount u0) ERR-INVALID-AMOUNT) ;; Amount must be greater than 0 - (let ((treasury-balance (var-get protocol-treasury))) - (asserts! (>= treasury-balance amount) ERR-INSUFFICIENT-TREASURY) ;; Insufficient treasury balance - ;; Update protocol treasury balance - (var-set protocol-treasury (- treasury-balance amount)) - ;; Transfer STX from contract to admin - (try! (stx-transfer? amount (as-contract tx-sender) admin-principal)) - ;; Emit event - (print { - event: "protocol-fees-withdrawn", - admin: admin-principal, - amount: amount, - remaining-balance: (- treasury-balance amount) - }) - ;; Log transaction - (let ((log-id (var-get next-log-id))) - (map-set transaction-logs log-id { - action: "withdraw-protocol-fees", - user: admin-principal, - event-id: none, - listing-id: none, - amount: (some amount), - metadata: "protocol-fees-withdrawn" - }) - (var-set next-log-id (+ log-id u1)) - ) - (ok true) - ) + (let ( + (caller tx-sender) + (admin-principal (var-get admin)) + ) + (asserts! (is-eq caller admin-principal) ERR-NOT-ADMIN) + ;; Only admin can withdraw + (asserts! (> amount u0) ERR-INVALID-AMOUNT) + ;; Amount must be greater than 0 + (let ((treasury-balance (var-get protocol-treasury))) + (asserts! (>= treasury-balance amount) ERR-INSUFFICIENT-TREASURY) + ;; Insufficient treasury balance + ;; Update protocol treasury balance + (var-set protocol-treasury (- treasury-balance amount)) + ;; Transfer STX from contract to admin + (try! (stx-transfer? amount (as-contract tx-sender) admin-principal)) + ;; Emit event + (print { + event: "protocol-fees-withdrawn", + admin: admin-principal, + amount: amount, + remaining-balance: (- treasury-balance amount), + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "withdraw-protocol-fees", + user: admin-principal, + event-id: none, + listing-id: none, + amount: (some amount), + metadata: "protocol-fees-withdrawn", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok true) ) + ) ) ;; ============================================================================ @@ -1335,30 +1636,44 @@ ;; - (ok true) on success ;; - ERR-GUILD-ID-EXISTS if guild ID already exists ;; ============================================================================ -(define-public (create-guild (guild-id uint) (name (string-ascii 50))) - (let ((creator tx-sender)) - (match (map-get? guilds guild-id) - existing ERR-GUILD-ID-EXISTS ;; Guild ID already exists - (begin - (map-set guilds guild-id { - creator: creator, - name: name, - total-points: u0, - member-count: u1 - }) - (map-set guild-members (tuple (guild-id guild-id) (user creator)) true) - (map-set guild-deposits (tuple (guild-id guild-id) (user creator)) u0) - ;; Emit event - (print { - event: "guild-created", - guild-id: guild-id, - creator: creator, - name: name - }) - (ok true) - ) +(define-public (create-guild + (guild-id uint) + (name (string-ascii 50)) + ) + (let ((creator tx-sender)) + (match (map-get? guilds guild-id) + existing + ERR-GUILD-ID-EXISTS ;; Guild ID already exists + (begin + (map-set guilds guild-id { + creator: creator, + name: name, + total-points: u0, + member-count: u1, + }) + (map-set guild-members { + guild-id: guild-id, + user: creator, + } + true + ) + (map-set guild-deposits { + guild-id: guild-id, + user: creator, + } + u0 ) + ;; Emit event + (print { + event: "guild-created", + guild-id: guild-id, + creator: creator, + name: name, + }) + (ok true) + ) ) + ) ) ;; ============================================================================ @@ -1385,32 +1700,43 @@ ;; - ERR-ALREADY-A-MEMBER if already a member ;; ============================================================================ (define-public (join-guild (guild-id uint)) - (let ((user tx-sender)) - (match (map-get? guilds guild-id) - guild (begin - (if (is-member? guild-id user) - ERR-ALREADY-A-MEMBER ;; Already a member - (begin - (map-set guild-members (tuple (guild-id guild-id) (user user)) true) - (map-set guild-deposits (tuple (guild-id guild-id) (user user)) u0) - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (get total-points guild), - member-count: (+ (get member-count guild) u1) - }) - (print { - event: "guild-joined", - guild-id: guild-id, - user: user - }) - (ok true) - ) - ) + (let ((user tx-sender)) + (match (map-get? guilds guild-id) + guild + (begin + (if (is-member? guild-id user) + ERR-ALREADY-A-MEMBER ;; Already a member + (begin + (map-set guild-members { + guild-id: guild-id, + user: user, + } + true + ) + (map-set guild-deposits { + guild-id: guild-id, + user: user, + } + u0 ) - ERR-GUILD-NOT-FOUND ;; Guild not found + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (get total-points guild), + member-count: (+ (get member-count guild) u1), + }) + (print { + event: "guild-joined", + guild-id: guild-id, + user: user, + }) + (ok true) + ) ) + ) + ERR-GUILD-NOT-FOUND ;; Guild not found ) + ) ) ;; ============================================================================ @@ -1438,53 +1764,67 @@ ;; - ERR-HAS-DEPOSITS if has deposits (must withdraw first) ;; ============================================================================ (define-public (leave-guild (guild-id uint)) - (let ((user tx-sender)) - (match (map-get? guilds guild-id) - guild (begin - (if (is-member? guild-id user) - (begin - (match (map-get? guild-deposits (tuple (guild-id guild-id) (user user))) - deposit (if (is-eq deposit u0) - (begin - (map-set guild-members (tuple (guild-id guild-id) (user user)) false) - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (get total-points guild), - member-count: (- (get member-count guild) u1) - }) - (print { - event: "guild-left", - guild-id: guild-id, - user: user - }) - (ok true) - ) - ERR-HAS-DEPOSITS ;; Has deposits, must withdraw first - ) - (begin - (map-set guild-members (tuple (guild-id guild-id) (user user)) false) - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (get total-points guild), - member-count: (- (get member-count guild) u1) - }) - (print { - event: "guild-left", - guild-id: guild-id, - user: user - }) - (ok true) - ) - ) - ) - ERR-NOT-A-MEMBER ;; Not a member - ) + (let ((user tx-sender)) + (match (map-get? guilds guild-id) + guild + (begin + (if (is-member? guild-id user) + (begin + (match (map-get? guild-deposits { + guild-id: guild-id, + user: user, + }) + deposit (if (is-eq deposit u0) + (begin + (map-set guild-members { + guild-id: guild-id, + user: user, + } + false + ) + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (get total-points guild), + member-count: (- (get member-count guild) u1), + }) + (print { + event: "guild-left", + guild-id: guild-id, + user: user, + }) + (ok true) + ) + ERR-HAS-DEPOSITS ;; Has deposits, must withdraw first + ) + (begin + (map-set guild-members { + guild-id: guild-id, + user: user, + } + false + ) + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (get total-points guild), + member-count: (- (get member-count guild) u1), + }) + (print { + event: "guild-left", + guild-id: guild-id, + user: user, + }) + (ok true) + ) ) - ERR-GUILD-NOT-FOUND ;; Guild not found + ) + ERR-NOT-A-MEMBER ;; Not a member ) + ) + ERR-GUILD-NOT-FOUND ;; Guild not found ) + ) ) ;; ============================================================================ @@ -1515,57 +1855,76 @@ ;; - ERR-GUILD-NOT-FOUND if guild not found ;; - ERR-NOT-A-MEMBER if not a member ;; ============================================================================ -(define-public (deposit-to-guild (guild-id uint) (amount uint)) - (let ((user tx-sender)) - (asserts! (> amount u0) ERR-INVALID-AMOUNT) ;; Amount must be greater than 0 - (match (map-get? guilds guild-id) - guild (begin - (if (is-member? guild-id user) - (begin - (match (map-get? user-points user) - current-points (begin - (asserts! (>= current-points amount) ERR-INSUFFICIENT-POINTS) ;; Insufficient points - ;; Deduct from user - (map-set user-points user (- current-points amount)) - ;; Add to guild pool - (match (map-get? guild-deposits (tuple (guild-id guild-id) (user user))) - existing-deposit (begin - (map-set guild-deposits (tuple (guild-id guild-id) (user user)) (+ existing-deposit amount)) - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (+ (get total-points guild) amount), - member-count: (get member-count guild) - }) - ) - (begin - (map-set guild-deposits (tuple (guild-id guild-id) (user user)) amount) - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (+ (get total-points guild) amount), - member-count: (get member-count guild) - }) - ) - ) - ;; Emit event - (print { - event: "guild-deposit", - guild-id: guild-id, - user: user, - amount: amount - }) - (ok true) - ) - ERR-USER-NOT-REGISTERED ;; User not registered - ) - ) - ERR-NOT-A-MEMBER ;; Not a member +(define-public (deposit-to-guild + (guild-id uint) + (amount uint) + ) + (let ((user tx-sender)) + (asserts! (> amount u0) ERR-INVALID-AMOUNT) + ;; Amount must be greater than 0 + (match (map-get? guilds guild-id) + guild + (begin + (if (is-member? guild-id user) + (begin + (match (map-get? user-points user) + current-points + (begin + (asserts! (>= current-points amount) ERR-INSUFFICIENT-POINTS) ;; Insufficient points + ;; Deduct from user + (map-set user-points user (- current-points amount)) + ;; Add to guild pool + (match (map-get? guild-deposits { + guild-id: guild-id, + user: user, + }) + existing-deposit (begin + (map-set guild-deposits { + guild-id: guild-id, + user: user, + } + (+ existing-deposit amount) + ) + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (+ (get total-points guild) amount), + member-count: (get member-count guild), + }) + ) + (begin + (map-set guild-deposits { + guild-id: guild-id, + user: user, + } + amount ) + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (+ (get total-points guild) amount), + member-count: (get member-count guild), + }) + ) + ) + ;; Emit event + (print { + event: "guild-deposit", + guild-id: guild-id, + user: user, + amount: amount, + }) + (ok true) + ) + ERR-USER-NOT-REGISTERED ;; User not registered ) - ERR-GUILD-NOT-FOUND ;; Guild not found + ) + ERR-NOT-A-MEMBER ;; Not a member ) + ) + ERR-GUILD-NOT-FOUND ;; Guild not found ) + ) ) ;; ============================================================================ @@ -1598,49 +1957,66 @@ ;; - ERR-NOT-A-MEMBER if not a member ;; - ERR-INSUFFICIENT-DEPOSITS if user has insufficient deposits ;; ============================================================================ -(define-public (withdraw-from-guild (guild-id uint) (amount uint)) - (let ((user tx-sender)) - (asserts! (> amount u0) ERR-INVALID-AMOUNT) ;; Amount must be greater than 0 - (match (map-get? guilds guild-id) - guild (begin - (if (is-member? guild-id user) - (begin - (match (map-get? guild-deposits (tuple (guild-id guild-id) (user user))) - user-deposit (begin - (asserts! (>= user-deposit amount) ERR-INSUFFICIENT-DEPOSITS) ;; Insufficient deposits - (asserts! (>= (get total-points guild) amount) ERR-INSUFFICIENT-POINTS) ;; Guild has insufficient points - ;; Deduct from guild pool - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (- (get total-points guild) amount), - member-count: (get member-count guild) - }) - ;; Update user's deposit record - (map-set guild-deposits (tuple (guild-id guild-id) (user user)) (- user-deposit amount)) - ;; Return points to user - (match (map-get? user-points user) - current-points (map-set user-points user (+ current-points amount)) - (map-set user-points user amount) - ) - ;; Emit event - (print { - event: "guild-withdraw", - guild-id: guild-id, - user: user, - amount: amount - }) - (ok true) - ) - ERR-INSUFFICIENT-DEPOSITS ;; No deposits - ) - ) - ERR-NOT-A-MEMBER ;; Not a member - ) +(define-public (withdraw-from-guild + (guild-id uint) + (amount uint) + ) + (let ((user tx-sender)) + (asserts! (> amount u0) ERR-INVALID-AMOUNT) + ;; Amount must be greater than 0 + (match (map-get? guilds guild-id) + guild + (begin + (if (is-member? guild-id user) + (begin + (match (map-get? guild-deposits { + guild-id: guild-id, + user: user, + }) + user-deposit + (begin + (asserts! (>= user-deposit amount) ERR-INSUFFICIENT-DEPOSITS) ;; Insufficient deposits + (asserts! (>= (get total-points guild) amount) + ERR-INSUFFICIENT-POINTS + ) + ;; Guild has insufficient points + ;; Deduct from guild pool + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (- (get total-points guild) amount), + member-count: (get member-count guild), + }) + ;; Update user's deposit record + (map-set guild-deposits { + guild-id: guild-id, + user: user, + } + (- user-deposit amount) + ) + ;; Return points to user + (match (map-get? user-points user) + current-points (map-set user-points user (+ current-points amount)) + (map-set user-points user amount) + ) + ;; Emit event + (print { + event: "guild-withdraw", + guild-id: guild-id, + user: user, + amount: amount, + }) + (ok true) + ) + ERR-INSUFFICIENT-DEPOSITS ;; No deposits ) - ERR-GUILD-NOT-FOUND ;; Guild not found + ) + ERR-NOT-A-MEMBER ;; Not a member ) + ) + ERR-GUILD-NOT-FOUND ;; Guild not found ) + ) ) ;; ============================================================================ @@ -1675,82 +2051,109 @@ ;; - ERR-GUILD-NOT-FOUND if guild not found ;; - ERR-NOT-A-MEMBER if not a member ;; ============================================================================ -(define-public (guild-stake-yes (guild-id uint) (event-id uint) (amount uint)) - (let ((user tx-sender)) - (asserts! (> amount u0) ERR-INVALID-AMOUNT) ;; Amount must be greater than 0 - (match (map-get? guilds guild-id) - guild (begin - (if (is-member? guild-id user) - (begin - (asserts! (>= (get total-points guild) amount) ERR-INSUFFICIENT-POINTS) ;; Insufficient guild points - (match (map-get? events event-id) - event (begin - (asserts! (is-eq (get status event) "open") ERR-EVENT-NOT-OPEN) ;; Event must be open - ;; Deduct from guild pool - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (- (get total-points guild) amount), - member-count: (get member-count guild) - }) - ;; Add to YES pool - (let ((new-yes-pool (+ (get yes-pool event) amount)) - (new-no-pool (get no-pool event))) - (map-set events event-id { - yes-pool: new-yes-pool, - no-pool: new-no-pool, - status: (get status event), - winner: (get winner event), - creator: (get creator event), - metadata: (get metadata event) - }) - ;; Record guild's stake - (match (map-get? guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id))) - existing-stake (map-set guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id)) (+ existing-stake amount)) - (begin - (map-set guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id)) amount) - ;; Track new prediction for guild leaderboard - (match (map-get? guild-stats guild-id) - stats (map-set guild-stats guild-id { - total-predictions: (+ (get total-predictions stats) u1), - wins: (get wins stats), - losses: (get losses stats), - total-points-earned: (get total-points-earned stats), - win-rate: (get win-rate stats) - }) - (map-set guild-stats guild-id { - total-predictions: u1, - wins: u0, - losses: u0, - total-points-earned: u0, - win-rate: u0 - }) - ) - ) - ) - ;; Update total guild YES stakes - (var-set total-guild-yes-stakes (+ (var-get total-guild-yes-stakes) amount)) - ;; Emit event - (print { - event: "guild-staked-yes", - guild-id: guild-id, - event-id: event-id, - amount: amount, - yes-pool: new-yes-pool, - no-pool: new-no-pool - }) - (ok true) - ) - ) - ERR-EVENT-NOT-FOUND ;; Event not found - ) - ) - ERR-NOT-A-MEMBER ;; Not a member +(define-public (guild-stake-yes + (guild-id uint) + (event-id uint) + (amount uint) + ) + (let ((user tx-sender)) + (asserts! (> amount u0) ERR-INVALID-AMOUNT) + ;; Amount must be greater than 0 + (match (map-get? guilds guild-id) + guild + (begin + (if (is-member? guild-id user) + (begin + (asserts! (>= (get total-points guild) amount) + ERR-INSUFFICIENT-POINTS + ) + ;; Insufficient guild points + (match (map-get? events event-id) + event + (begin + (asserts! (is-eq (get status event) "open") ERR-EVENT-NOT-OPEN) ;; Event must be open + ;; Deduct from guild pool + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (- (get total-points guild) amount), + member-count: (get member-count guild), + }) + ;; Add to YES pool + (let ( + (new-yes-pool (+ (get yes-pool event) amount)) + (new-no-pool (get no-pool event)) + ) + (map-set events event-id { + yes-pool: new-yes-pool, + no-pool: new-no-pool, + status: (get status event), + winner: (get winner event), + creator: (get creator event), + metadata: (get metadata event), + }) + ;; Record guild's stake + (match (map-get? guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + }) + existing-stake (map-set guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + } + (+ existing-stake amount) + ) + (begin + (map-set guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + } + amount + ) + ;; Track new prediction for guild leaderboard + (match (map-get? guild-stats guild-id) + stats (map-set guild-stats guild-id { + total-predictions: (+ (get total-predictions stats) u1), + wins: (get wins stats), + losses: (get losses stats), + total-points-earned: (get total-points-earned stats), + win-rate: (get win-rate stats), + }) + (map-set guild-stats guild-id { + total-predictions: u1, + wins: u0, + losses: u0, + total-points-earned: u0, + win-rate: u0, + }) + ) ) + ) + ;; Update total guild YES stakes + (var-set total-guild-yes-stakes + (+ (var-get total-guild-yes-stakes) amount) + ) + ;; Emit event + (print { + event: "guild-staked-yes", + guild-id: guild-id, + event-id: event-id, + amount: amount, + yes-pool: new-yes-pool, + no-pool: new-no-pool, + }) + (ok true) + ) + ) + ERR-EVENT-NOT-FOUND ;; Event not found ) - ERR-GUILD-NOT-FOUND ;; Guild not found + ) + ERR-NOT-A-MEMBER ;; Not a member ) + ) + ERR-GUILD-NOT-FOUND ;; Guild not found ) + ) ) ;; ============================================================================ @@ -1778,82 +2181,109 @@ ;; - ERR-GUILD-NOT-FOUND if guild not found ;; - ERR-NOT-A-MEMBER if not a member ;; ============================================================================ -(define-public (guild-stake-no (guild-id uint) (event-id uint) (amount uint)) - (let ((user tx-sender)) - (asserts! (> amount u0) ERR-INVALID-AMOUNT) ;; Amount must be greater than 0 - (match (map-get? guilds guild-id) - guild (begin - (if (is-member? guild-id user) - (begin - (asserts! (>= (get total-points guild) amount) ERR-INSUFFICIENT-POINTS) ;; Insufficient guild points - (match (map-get? events event-id) - event (begin - (asserts! (is-eq (get status event) "open") ERR-EVENT-NOT-OPEN) ;; Event must be open - ;; Deduct from guild pool - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (- (get total-points guild) amount), - member-count: (get member-count guild) - }) - ;; Add to NO pool - (let ((new-yes-pool (get yes-pool event)) - (new-no-pool (+ (get no-pool event) amount))) - (map-set events event-id { - yes-pool: new-yes-pool, - no-pool: new-no-pool, - status: (get status event), - winner: (get winner event), - creator: (get creator event), - metadata: (get metadata event) - }) - ;; Record guild's stake - (match (map-get? guild-no-stakes (tuple (guild-id guild-id) (event-id event-id))) - existing-stake (map-set guild-no-stakes (tuple (guild-id guild-id) (event-id event-id)) (+ existing-stake amount)) - (begin - (map-set guild-no-stakes (tuple (guild-id guild-id) (event-id event-id)) amount) - ;; Track new prediction for guild leaderboard - (match (map-get? guild-stats guild-id) - stats (map-set guild-stats guild-id { - total-predictions: (+ (get total-predictions stats) u1), - wins: (get wins stats), - losses: (get losses stats), - total-points-earned: (get total-points-earned stats), - win-rate: (get win-rate stats) - }) - (map-set guild-stats guild-id { - total-predictions: u1, - wins: u0, - losses: u0, - total-points-earned: u0, - win-rate: u0 - }) - ) - ) - ) - ;; Update total guild NO stakes - (var-set total-guild-no-stakes (+ (var-get total-guild-no-stakes) amount)) - ;; Emit event - (print { - event: "guild-staked-no", - guild-id: guild-id, - event-id: event-id, - amount: amount, - yes-pool: new-yes-pool, - no-pool: new-no-pool - }) - (ok true) - ) - ) - ERR-EVENT-NOT-FOUND ;; Event not found - ) - ) - ERR-NOT-A-MEMBER ;; Not a member +(define-public (guild-stake-no + (guild-id uint) + (event-id uint) + (amount uint) + ) + (let ((user tx-sender)) + (asserts! (> amount u0) ERR-INVALID-AMOUNT) + ;; Amount must be greater than 0 + (match (map-get? guilds guild-id) + guild + (begin + (if (is-member? guild-id user) + (begin + (asserts! (>= (get total-points guild) amount) + ERR-INSUFFICIENT-POINTS + ) + ;; Insufficient guild points + (match (map-get? events event-id) + event + (begin + (asserts! (is-eq (get status event) "open") ERR-EVENT-NOT-OPEN) ;; Event must be open + ;; Deduct from guild pool + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (- (get total-points guild) amount), + member-count: (get member-count guild), + }) + ;; Add to NO pool + (let ( + (new-yes-pool (get yes-pool event)) + (new-no-pool (+ (get no-pool event) amount)) + ) + (map-set events event-id { + yes-pool: new-yes-pool, + no-pool: new-no-pool, + status: (get status event), + winner: (get winner event), + creator: (get creator event), + metadata: (get metadata event), + }) + ;; Record guild's stake + (match (map-get? guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + }) + existing-stake (map-set guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + } + (+ existing-stake amount) + ) + (begin + (map-set guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + } + amount + ) + ;; Track new prediction for guild leaderboard + (match (map-get? guild-stats guild-id) + stats (map-set guild-stats guild-id { + total-predictions: (+ (get total-predictions stats) u1), + wins: (get wins stats), + losses: (get losses stats), + total-points-earned: (get total-points-earned stats), + win-rate: (get win-rate stats), + }) + (map-set guild-stats guild-id { + total-predictions: u1, + wins: u0, + losses: u0, + total-points-earned: u0, + win-rate: u0, + }) + ) ) + ) + ;; Update total guild NO stakes + (var-set total-guild-no-stakes + (+ (var-get total-guild-no-stakes) amount) + ) + ;; Emit event + (print { + event: "guild-staked-no", + guild-id: guild-id, + event-id: event-id, + amount: amount, + yes-pool: new-yes-pool, + no-pool: new-no-pool, + }) + (ok true) + ) + ) + ERR-EVENT-NOT-FOUND ;; Event not found ) - ERR-GUILD-NOT-FOUND ;; Guild not found + ) + ERR-NOT-A-MEMBER ;; Not a member ) + ) + ERR-GUILD-NOT-FOUND ;; Guild not found ) + ) ) ;; ============================================================================ @@ -1888,318 +2318,454 @@ ;; - ERR-GUILD-NOT-FOUND if guild not found ;; - ERR-NOT-A-MEMBER if not a member ;; ============================================================================ -(define-public (guild-claim (guild-id uint) (event-id uint)) - (let ((user tx-sender)) - (match (map-get? guilds guild-id) - guild (begin - (if (is-member? guild-id user) +(define-public (guild-claim + (guild-id uint) + (event-id uint) + ) + (let ((user tx-sender)) + (match (map-get? guilds guild-id) + guild + (begin + (if (is-member? guild-id user) + (begin + (match (map-get? events event-id) + event + (begin + (asserts! (is-eq (get status event) "resolved") + ERR-EVENT-MUST-BE-RESOLVED + ) + ;; Event must be resolved + (match (get winner event) + winner + (begin + (let ( + (yes-pool (get yes-pool event)) + (no-pool (get no-pool event)) + (total-pool (+ yes-pool no-pool)) + (winning-pool (if winner + yes-pool + no-pool + )) + ) + (if (is-eq winning-pool u0) + ERR-NO-WINNERS ;; No winners (pool is empty) (begin - (match (map-get? events event-id) - event (begin - (asserts! (is-eq (get status event) "resolved") ERR-EVENT-MUST-BE-RESOLVED) ;; Event must be resolved - (match (get winner event) - winner (begin - (let ((yes-pool (get yes-pool event)) - (no-pool (get no-pool event)) - (total-pool (+ yes-pool no-pool)) - (winning-pool (if winner yes-pool no-pool))) - (if (is-eq winning-pool u0) - ERR-NO-WINNERS ;; No winners (pool is empty) - (begin - (if winner - ;; Guild staked YES - (match (map-get? guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id))) - stake (begin - (if (> stake u0) - (let ((reward (/ (* stake total-pool) winning-pool))) - ;; Add reward to guild pool - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (+ (get total-points guild) reward), - member-count: (get member-count guild) - }) - ;; Update total guild YES stakes (subtract before clearing) - (var-set total-guild-yes-stakes (- (var-get total-guild-yes-stakes) stake)) - ;; Clear the stake - (map-set guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id)) u0) - ;; Update guild leaderboard stats (WIN) - (match (map-get? guild-stats guild-id) - stats (begin - (let ((new-wins (+ (get wins stats) u1)) - (new-total-earned (+ (get total-points-earned stats) reward)) - (new-win-rate (/ (* new-wins u10000) (+ new-wins (get losses stats))))) - (map-set guild-stats guild-id { - total-predictions: (get total-predictions stats), - wins: new-wins, - losses: (get losses stats), - total-points-earned: new-total-earned, - win-rate: new-win-rate - }) - ) - ) - (map-set guild-stats guild-id { - total-predictions: u1, - wins: u1, - losses: u0, - total-points-earned: reward, - win-rate: u10000 - }) - ) - ;; Emit event - (print { - event: "guild-reward-claimed", - guild-id: guild-id, - event-id: event-id, - reward: reward, - total-points: (+ (get total-points guild) reward) - }) - (ok reward) - ) - (begin - ;; Guild had YES stake but it's 0, check if guild had NO stake (they lost) - (match (map-get? guild-no-stakes (tuple (guild-id guild-id) (event-id event-id))) - no-stake (begin - (if (> no-stake u0) - (begin - ;; Guild had NO stake but YES won - clear stake and track loss - ;; Update total guild NO stakes (subtract before clearing) - (var-set total-guild-no-stakes (- (var-get total-guild-no-stakes) no-stake)) - (map-set guild-no-stakes (tuple (guild-id guild-id) (event-id event-id)) u0) - ;; Update guild leaderboard stats (LOSS) - (match (map-get? guild-stats guild-id) - stats (begin - (let ((new-losses (+ (get losses stats) u1)) - (total-games (+ (get wins stats) new-losses)) - (new-win-rate (if (is-eq total-games u0) u0 (/ (* (get wins stats) u10000) total-games)))) - (map-set guild-stats guild-id { - total-predictions: (get total-predictions stats), - wins: (get wins stats), - losses: new-losses, - total-points-earned: (get total-points-earned stats), - win-rate: new-win-rate - }) - ) - ) - (map-set guild-stats guild-id { - total-predictions: u1, - wins: u0, - losses: u1, - total-points-earned: u0, - win-rate: u0 - }) - ) - ;; Return success with 0 reward to indicate loss tracked (state changes persist) - (ok u0) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ) - ) - (begin - ;; No YES stake found, check if guild had NO stake (they lost) - (let ((no-stake-opt (map-get? guild-no-stakes (tuple (guild-id guild-id) (event-id event-id))))) - (if (is-some no-stake-opt) - (let ((no-stake (unwrap! no-stake-opt ERR-NO-STAKE-FOUND))) - (if (> no-stake u0) - (begin - ;; Guild had NO stake but YES won - clear stake and track loss - ;; Update total guild NO stakes (subtract before clearing) - (var-set total-guild-no-stakes (- (var-get total-guild-no-stakes) no-stake)) - (map-set guild-no-stakes (tuple (guild-id guild-id) (event-id event-id)) u0) - ;; Update guild leaderboard stats (LOSS) - (match (map-get? guild-stats guild-id) - stats (begin - (let ((new-losses (+ (get losses stats) u1)) - (total-games (+ (get wins stats) new-losses)) - (new-win-rate (if (is-eq total-games u0) u0 (/ (* (get wins stats) u10000) total-games)))) - (map-set guild-stats guild-id { - total-predictions: (get total-predictions stats), - wins: (get wins stats), - losses: new-losses, - total-points-earned: (get total-points-earned stats), - win-rate: new-win-rate - }) - ) - ) - (map-set guild-stats guild-id { - total-predictions: u1, - wins: u0, - losses: u1, - total-points-earned: u0, - win-rate: u0 - }) - ) - ;; Return success with 0 reward to indicate loss tracked (state changes persist) - (ok u0) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ) - ) - ;; Guild staked NO - (match (map-get? guild-no-stakes (tuple (guild-id guild-id) (event-id event-id))) - stake (begin - (if (> stake u0) - (let ((reward (/ (* stake total-pool) winning-pool))) - ;; Add reward to guild pool - (map-set guilds guild-id { - creator: (get creator guild), - name: (get name guild), - total-points: (+ (get total-points guild) reward), - member-count: (get member-count guild) - }) - ;; Update total guild NO stakes (subtract before clearing) - (var-set total-guild-no-stakes (- (var-get total-guild-no-stakes) stake)) - ;; Clear the stake - (map-set guild-no-stakes (tuple (guild-id guild-id) (event-id event-id)) u0) - ;; Update guild leaderboard stats (WIN) - (match (map-get? guild-stats guild-id) - stats (begin - (let ((new-wins (+ (get wins stats) u1)) - (new-total-earned (+ (get total-points-earned stats) reward)) - (new-win-rate (/ (* new-wins u10000) (+ new-wins (get losses stats))))) - (map-set guild-stats guild-id { - total-predictions: (get total-predictions stats), - wins: new-wins, - losses: (get losses stats), - total-points-earned: new-total-earned, - win-rate: new-win-rate - }) - ) - ) - (map-set guild-stats guild-id { - total-predictions: u1, - wins: u1, - losses: u0, - total-points-earned: reward, - win-rate: u10000 - }) - ) - ;; Emit event - (print { - event: "guild-reward-claimed", - guild-id: guild-id, - event-id: event-id, - reward: reward, - total-points: (+ (get total-points guild) reward) - }) - (ok reward) - ) - (begin - ;; Guild had NO stake but it's 0, check if guild had YES stake (they lost) - (match (map-get? guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id))) - yes-stake (begin - (if (> yes-stake u0) - (begin - ;; Guild had YES stake but NO won - clear stake and track loss - ;; Update total guild YES stakes (subtract before clearing) - (var-set total-guild-yes-stakes (- (var-get total-guild-yes-stakes) yes-stake)) - (map-set guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id)) u0) - ;; Update guild leaderboard stats (LOSS) - (match (map-get? guild-stats guild-id) - stats (begin - (let ((new-losses (+ (get losses stats) u1)) - (total-games (+ (get wins stats) new-losses)) - (new-win-rate (if (is-eq total-games u0) u0 (/ (* (get wins stats) u10000) total-games)))) - (map-set guild-stats guild-id { - total-predictions: (get total-predictions stats), - wins: (get wins stats), - losses: new-losses, - total-points-earned: (get total-points-earned stats), - win-rate: new-win-rate - }) - ) - ) - (map-set guild-stats guild-id { - total-predictions: u1, - wins: u0, - losses: u1, - total-points-earned: u0, - win-rate: u0 - }) - ) - ;; Return success with 0 reward to indicate loss tracked (state changes persist) - (ok u0) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ) - ) - (begin - ;; No NO stake found, check if guild had YES stake (they lost) - (let ((yes-stake-opt (map-get? guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id))))) - (if (is-some yes-stake-opt) - (let ((yes-stake (unwrap! yes-stake-opt ERR-NO-STAKE-FOUND))) - (if (> yes-stake u0) - (begin - ;; Guild had YES stake but NO won - clear stake and track loss - ;; Update total guild YES stakes (subtract before clearing) - (var-set total-guild-yes-stakes (- (var-get total-guild-yes-stakes) yes-stake)) - (map-set guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id)) u0) - ;; Update guild leaderboard stats (LOSS) - (match (map-get? guild-stats guild-id) - stats (begin - (let ((new-losses (+ (get losses stats) u1)) - (total-games (+ (get wins stats) new-losses)) - (new-win-rate (if (is-eq total-games u0) u0 (/ (* (get wins stats) u10000) total-games)))) - (map-set guild-stats guild-id { - total-predictions: (get total-predictions stats), - wins: (get wins stats), - losses: new-losses, - total-points-earned: (get total-points-earned stats), - win-rate: new-win-rate - }) - ) - ) - (map-set guild-stats guild-id { - total-predictions: u1, - wins: u0, - losses: u1, - total-points-earned: u0, - win-rate: u0 - }) - ) - ;; Return success with 0 reward to indicate loss tracked (state changes persist) - (ok u0) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ERR-NO-STAKE-FOUND ;; No stake found - ) - ) - ) - ) + (if winner + ;; Guild staked YES + (match (map-get? guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + }) + stake (begin + (if (> stake u0) + (let ((reward (/ (* stake total-pool) winning-pool))) + ;; Add reward to guild pool + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (+ (get total-points guild) reward), + member-count: (get member-count guild), + }) + ;; Update total guild YES stakes (subtract before clearing) + (var-set total-guild-yes-stakes + (- (var-get total-guild-yes-stakes) stake) + ) + ;; Clear the stake + (map-set guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + } + u0 + ) + ;; Update guild leaderboard stats (WIN) + (match (map-get? guild-stats guild-id) + stats (begin + (let ( + (new-wins (+ (get wins stats) u1)) + (new-total-earned (+ (get total-points-earned stats) + reward + )) + (new-win-rate (/ (* new-wins u10000) + (+ new-wins (get losses stats)) + )) + ) + (map-set guild-stats guild-id { + total-predictions: (get total-predictions stats), + wins: new-wins, + losses: (get losses stats), + total-points-earned: new-total-earned, + win-rate: new-win-rate, + }) + ) + ) + (map-set guild-stats guild-id { + total-predictions: u1, + wins: u1, + losses: u0, + total-points-earned: reward, + win-rate: u10000, + }) + ) + ;; Emit event + (print { + event: "guild-reward-claimed", + guild-id: guild-id, + event-id: event-id, + reward: reward, + total-points: (+ (get total-points guild) reward), + }) + (ok reward) + ) + (begin + ;; Guild had YES stake but it's 0, check if guild had NO stake (they lost) + (match (map-get? guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + }) + no-stake + (begin + (if (> no-stake u0) + (begin + ;; Guild had NO stake but YES won - clear stake and track loss + ;; Update total guild NO stakes (subtract before clearing) + (var-set total-guild-no-stakes + (- (var-get total-guild-no-stakes) + no-stake + )) + (map-set guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + } + u0 + ) + ;; Update guild leaderboard stats (LOSS) + (match (map-get? guild-stats guild-id) + stats (begin + (let ( + (new-losses (+ (get losses stats) u1)) + (total-games (+ (get wins stats) + new-losses + )) + (new-win-rate (if (is-eq total-games u0) + u0 + (/ + (* (get wins stats) + u10000 ) + total-games + ) + )) + ) + (map-set guild-stats guild-id { + total-predictions: (get total-predictions stats), + wins: (get wins stats), + losses: new-losses, + total-points-earned: (get total-points-earned + stats + ), + win-rate: new-win-rate, + }) + ) + ) + (map-set guild-stats guild-id { + total-predictions: u1, + wins: u0, + losses: u1, + total-points-earned: u0, + win-rate: u0, + }) + ) + ;; Return success with 0 reward to indicate loss tracked (state changes persist) + (ok u0) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ) + ) + (begin + ;; No YES stake found, check if guild had NO stake (they lost) + (let ((no-stake-opt (map-get? guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + }))) + (if (is-some no-stake-opt) + (let ((no-stake (unwrap! no-stake-opt ERR-NO-STAKE-FOUND))) + (if (> no-stake u0) + (begin + ;; Guild had NO stake but YES won - clear stake and track loss + ;; Update total guild NO stakes (subtract before clearing) + (var-set total-guild-no-stakes + (- (var-get total-guild-no-stakes) + no-stake + )) + (map-set guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + } + u0 + ) + ;; Update guild leaderboard stats (LOSS) + (match (map-get? guild-stats guild-id) + stats (begin + (let ( + (new-losses (+ (get losses stats) u1)) + (total-games (+ (get wins stats) new-losses)) + (new-win-rate (if (is-eq total-games u0) + u0 + (/ + (* (get wins stats) u10000) + total-games ) + )) ) + (map-set guild-stats guild-id { + total-predictions: (get total-predictions stats), + wins: (get wins stats), + losses: new-losses, + total-points-earned: (get total-points-earned stats), + win-rate: new-win-rate, + }) + ) ) + (map-set guild-stats guild-id { + total-predictions: u1, + wins: u0, + losses: u1, + total-points-earned: u0, + win-rate: u0, + }) + ) + ;; Return success with 0 reward to indicate loss tracked (state changes persist) + (ok u0) ) - ERR-WINNER-NOT-SET ;; Winner not set + ERR-NO-STAKE-FOUND ;; No stake found + ) ) + ERR-NO-STAKE-FOUND ;; No stake found + ) ) - ERR-EVENT-NOT-FOUND ;; Event not found + ) ) + ;; Guild staked NO + (match (map-get? guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + }) + stake (begin + (if (> stake u0) + (let ((reward (/ (* stake total-pool) winning-pool))) + ;; Add reward to guild pool + (map-set guilds guild-id { + creator: (get creator guild), + name: (get name guild), + total-points: (+ (get total-points guild) reward), + member-count: (get member-count guild), + }) + ;; Update total guild NO stakes (subtract before clearing) + (var-set total-guild-no-stakes + (- (var-get total-guild-no-stakes) stake) + ) + ;; Clear the stake + (map-set guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + } + u0 + ) + ;; Update guild leaderboard stats (WIN) + (match (map-get? guild-stats guild-id) + stats (begin + (let ( + (new-wins (+ (get wins stats) u1)) + (new-total-earned (+ (get total-points-earned stats) + reward + )) + (new-win-rate (/ (* new-wins u10000) + (+ new-wins (get losses stats)) + )) + ) + (map-set guild-stats guild-id { + total-predictions: (get total-predictions stats), + wins: new-wins, + losses: (get losses stats), + total-points-earned: new-total-earned, + win-rate: new-win-rate, + }) + ) + ) + (map-set guild-stats guild-id { + total-predictions: u1, + wins: u1, + losses: u0, + total-points-earned: reward, + win-rate: u10000, + }) + ) + ;; Emit event + (print { + event: "guild-reward-claimed", + guild-id: guild-id, + event-id: event-id, + reward: reward, + total-points: (+ (get total-points guild) reward), + }) + (ok reward) + ) + (begin + ;; Guild had NO stake but it's 0, check if guild had YES stake (they lost) + (match (map-get? guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + }) + yes-stake + (begin + (if (> yes-stake u0) + (begin + ;; Guild had YES stake but NO won - clear stake and track loss + ;; Update total guild YES stakes (subtract before clearing) + (var-set total-guild-yes-stakes + (- (var-get total-guild-yes-stakes) + yes-stake + )) + (map-set guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + } + u0 + ) + ;; Update guild leaderboard stats (LOSS) + (match (map-get? guild-stats guild-id) + stats (begin + (let ( + (new-losses (+ (get losses stats) u1)) + (total-games (+ (get wins stats) + new-losses + )) + (new-win-rate (if (is-eq total-games u0) + u0 + (/ + (* (get wins stats) + u10000 + ) + total-games + ) + )) + ) + (map-set guild-stats guild-id { + total-predictions: (get total-predictions stats), + wins: (get wins stats), + losses: new-losses, + total-points-earned: (get total-points-earned + stats + ), + win-rate: new-win-rate, + }) + ) + ) + (map-set guild-stats guild-id { + total-predictions: u1, + wins: u0, + losses: u1, + total-points-earned: u0, + win-rate: u0, + }) + ) + ;; Return success with 0 reward to indicate loss tracked (state changes persist) + (ok u0) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ) + ) + (begin + ;; No NO stake found, check if guild had YES stake (they lost) + (let ((yes-stake-opt (map-get? guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + }))) + (if (is-some yes-stake-opt) + (let ((yes-stake (unwrap! yes-stake-opt ERR-NO-STAKE-FOUND))) + (if (> yes-stake u0) + (begin + ;; Guild had YES stake but NO won - clear stake and track loss + ;; Update total guild YES stakes (subtract before clearing) + (var-set total-guild-yes-stakes + (- (var-get total-guild-yes-stakes) + yes-stake + )) + (map-set guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + } + u0 + ) + ;; Update guild leaderboard stats (LOSS) + (match (map-get? guild-stats guild-id) + stats (begin + (let ( + (new-losses (+ (get losses stats) u1)) + (total-games (+ (get wins stats) new-losses)) + (new-win-rate (if (is-eq total-games u0) + u0 + (/ + (* (get wins stats) u10000) + total-games + ) + )) + ) + (map-set guild-stats guild-id { + total-predictions: (get total-predictions stats), + wins: (get wins stats), + losses: new-losses, + total-points-earned: (get total-points-earned stats), + win-rate: new-win-rate, + }) + ) + ) + (map-set guild-stats guild-id { + total-predictions: u1, + wins: u0, + losses: u1, + total-points-earned: u0, + win-rate: u0, + }) + ) + ;; Return success with 0 reward to indicate loss tracked (state changes persist) + (ok u0) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ERR-NO-STAKE-FOUND ;; No stake found + ) + ) + ) + ) + ) ) - ERR-NOT-A-MEMBER ;; Not a member + ) ) + ) + ERR-WINNER-NOT-SET ;; Winner not set + ) + ) + ERR-EVENT-NOT-FOUND ;; Event not found ) - ERR-GUILD-NOT-FOUND ;; Guild not found + ) + ERR-NOT-A-MEMBER ;; Not a member ) + ) + ERR-GUILD-NOT-FOUND ;; Guild not found ) + ) ) ;; read only functions @@ -2219,10 +2785,10 @@ ;; - (ok none) if user is not registered ;; ============================================================================ (define-read-only (get-user-points (user principal)) - (match (map-get? user-points user) - points-opt (ok (some points-opt)) - (ok none) - ) + (match (map-get? user-points user) + points-opt (ok (some points-opt)) + (ok none) + ) ) ;; ============================================================================ @@ -2242,10 +2808,10 @@ ;; Note: Earned points count toward the 10,000 threshold needed to sell points. ;; ============================================================================ (define-read-only (get-earned-points (user principal)) - (match (map-get? earned-points user) - earned-opt (ok (some earned-opt)) - (ok none) - ) + (match (map-get? earned-points user) + earned-opt (ok (some earned-opt)) + (ok none) + ) ) ;; ============================================================================ @@ -2263,7 +2829,7 @@ ;; - none if user is not registered ;; ============================================================================ (define-read-only (get-username (user principal)) - (map-get? user-names user) + (map-get? user-names user) ) ;; ============================================================================ @@ -2281,10 +2847,10 @@ ;; - (ok false) if user has not earned enough points or is not registered ;; ============================================================================ (define-read-only (can-sell (user principal)) - (match (map-get? earned-points user) - earned (ok (>= earned MIN_EARNED_FOR_SELL)) - (ok false) - ) + (match (map-get? earned-points user) + earned (ok (>= earned MIN_EARNED_FOR_SELL)) + (ok false) + ) ) ;; ============================================================================ @@ -2309,7 +2875,7 @@ ;; - none if event not found ;; ============================================================================ (define-read-only (get-event (event-id uint)) - (map-get? events event-id) + (map-get? events event-id) ) ;; ============================================================================ @@ -2327,8 +2893,14 @@ ;; - (some stake-amount) if user has staked on YES ;; - none if user has no YES stake ;; ============================================================================ -(define-read-only (get-yes-stake (event-id uint) (user principal)) - (map-get? yes-stakes (tuple (event-id event-id) (user user))) +(define-read-only (get-yes-stake + (event-id uint) + (user principal) + ) + (map-get? yes-stakes { + event-id: event-id, + user: user, + }) ) ;; ============================================================================ @@ -2346,8 +2918,14 @@ ;; - (some stake-amount) if user has staked on NO ;; - none if user has no NO stake ;; ============================================================================ -(define-read-only (get-no-stake (event-id uint) (user principal)) - (map-get? no-stakes (tuple (event-id event-id) (user user))) +(define-read-only (get-no-stake + (event-id uint) + (user principal) + ) + (map-get? no-stakes { + event-id: event-id, + user: user, + }) ) ;; ============================================================================ @@ -2358,7 +2936,7 @@ ;; Returns: uint - Total YES stakes across all events ;; ============================================================================ (define-read-only (get-total-yes-stakes) - (var-get total-yes-stakes) + (var-get total-yes-stakes) ) ;; ============================================================================ @@ -2369,7 +2947,7 @@ ;; Returns: uint - Total NO stakes across all events ;; ============================================================================ (define-read-only (get-total-no-stakes) - (var-get total-no-stakes) + (var-get total-no-stakes) ) ;; ============================================================================ @@ -2391,7 +2969,7 @@ ;; - none if listing not found ;; ============================================================================ (define-read-only (get-listing (listing-id uint)) - (map-get? listings listing-id) + (map-get? listings listing-id) ) ;; ============================================================================ @@ -2407,7 +2985,7 @@ ;; Note: Treasury accumulates 2% fees from each marketplace point sale. ;; ============================================================================ (define-read-only (get-protocol-treasury) - (ok (var-get protocol-treasury)) + (ok (var-get protocol-treasury)) ) ;; ============================================================================ @@ -2423,7 +3001,7 @@ ;; Note: Admin can create events and resolve them. ;; ============================================================================ (define-read-only (get-admin) - (ok (var-get admin)) + (ok (var-get admin)) ) ;; ============================================================================ @@ -2445,7 +3023,7 @@ ;; - none if guild not found ;; ============================================================================ (define-read-only (get-guild (guild-id uint)) - (map-get? guilds guild-id) + (map-get? guilds guild-id) ) ;; ============================================================================ @@ -2463,8 +3041,14 @@ ;; - (some true) if user is a member ;; - none if user is not a member ;; ============================================================================ -(define-read-only (is-guild-member (guild-id uint) (user principal)) - (map-get? guild-members (tuple (guild-id guild-id) (user user))) +(define-read-only (is-guild-member + (guild-id uint) + (user principal) + ) + (map-get? guild-members { + guild-id: guild-id, + user: user, + }) ) ;; ============================================================================ @@ -2482,8 +3066,14 @@ ;; - (some deposit-amount) if user has deposits ;; - none if user has no deposits ;; ============================================================================ -(define-read-only (get-guild-deposit (guild-id uint) (user principal)) - (map-get? guild-deposits (tuple (guild-id guild-id) (user user))) +(define-read-only (get-guild-deposit + (guild-id uint) + (user principal) + ) + (map-get? guild-deposits { + guild-id: guild-id, + user: user, + }) ) ;; ============================================================================ @@ -2501,8 +3091,14 @@ ;; - (some stake-amount) if guild has staked on YES ;; - none if guild has no YES stake ;; ============================================================================ -(define-read-only (get-guild-yes-stake (guild-id uint) (event-id uint)) - (map-get? guild-yes-stakes (tuple (guild-id guild-id) (event-id event-id))) +(define-read-only (get-guild-yes-stake + (guild-id uint) + (event-id uint) + ) + (map-get? guild-yes-stakes { + guild-id: guild-id, + event-id: event-id, + }) ) ;; ============================================================================ @@ -2520,8 +3116,14 @@ ;; - (some stake-amount) if guild has staked on NO ;; - none if guild has no NO stake ;; ============================================================================ -(define-read-only (get-guild-no-stake (guild-id uint) (event-id uint)) - (map-get? guild-no-stakes (tuple (guild-id guild-id) (event-id event-id))) +(define-read-only (get-guild-no-stake + (guild-id uint) + (event-id uint) + ) + (map-get? guild-no-stakes { + guild-id: guild-id, + event-id: event-id, + }) ) ;; ============================================================================ @@ -2532,7 +3134,7 @@ ;; Returns: uint - Total guild YES stakes across all events ;; ============================================================================ (define-read-only (get-total-guild-yes-stakes) - (var-get total-guild-yes-stakes) + (var-get total-guild-yes-stakes) ) ;; ============================================================================ @@ -2543,7 +3145,7 @@ ;; Returns: uint - Total guild NO stakes across all events ;; ============================================================================ (define-read-only (get-total-guild-no-stakes) - (var-get total-guild-no-stakes) + (var-get total-guild-no-stakes) ) ;; ============================================================================ @@ -2568,7 +3170,7 @@ ;; Note: Use this to build user leaderboards showing prediction performance. ;; ============================================================================ (define-read-only (get-user-stats (user principal)) - (map-get? user-stats user) + (map-get? user-stats user) ) ;; ============================================================================ @@ -2593,7 +3195,7 @@ ;; Note: Use this to build guild leaderboards showing collaborative prediction performance. ;; ============================================================================ (define-read-only (get-guild-stats (guild-id uint)) - (map-get? guild-stats guild-id) + (map-get? guild-stats guild-id) ) ;; ============================================================================ @@ -2620,17 +3222,24 @@ ;; Frontend applications can query this to track contract activity. ;; ============================================================================ (define-read-only (get-transaction-log (log-id uint)) - (map-get? transaction-logs log-id) + (map-get? transaction-logs log-id) ) ;; private functions ;; Helper function to check if user is guild member -(define-private (is-member? (guild-id uint) (user principal)) - (match (map-get? guild-members (tuple (guild-id guild-id) (user user))) - member-status member-status - false - ) +(define-private (is-member? + (guild-id uint) + (user principal) + ) + (match (map-get? guild-members { + guild-id: guild-id, + user: user, + }) + member-status + member-status + false + ) ) ;; ============================================================================ @@ -2653,16 +3262,18 @@ ;; Returns: ;; - (ok true) on success ;; ============================================================================ -(define-private (increase-points (user principal) (amount uint)) - (match (map-get? user-points user) - current-points (begin - (map-set user-points user (+ current-points amount)) - (ok true) - ) - (begin - (map-set user-points user amount) - (ok true) - ) +(define-private (increase-points + (user principal) + (amount uint) + ) + (match (map-get? user-points user) + current-points (begin + (map-set user-points user (+ current-points amount)) + (ok true) + ) + (begin + (map-set user-points user amount) + (ok true) ) + ) ) - diff --git a/contracts/roxy.tests.clar b/contracts/roxy.tests.clar new file mode 100644 index 0000000..aad1bbb --- /dev/null +++ b/contracts/roxy.tests.clar @@ -0,0 +1,1184 @@ +;; title: Roxy Tests +;; version: 1.0.0 +;; summary: Rendezvous fuzzing test suite for Roxy contract +;; description: Property-based testing for Bitcoin L2 Prediction Market Game with Points System and Marketplace + +;; ============================================================================= +;; PROPERTY-BASED TESTS FOR RENDEZVOUS +;; ============================================================================= + +;; Basic Fuzzing Tests - Input Validation and Error Handling +;; These tests ensure the contract can handle various input types without crashing + +;; Property: User registration input validation +(define-public (test-register-fuzz (username (string-ascii 50))) + (begin + (unwrap! (register username) (ok false)) + (ok true) + ) +) + +;; Property: Event creation input validation (admin only - may fail) +(define-public (test-create-event-fuzz (event-id uint) (metadata (string-ascii 200))) + (begin + (unwrap! (create-event event-id metadata) (ok false)) + (ok true) + ) +) + +;; Property: YES staking input validation +(define-public (test-stake-yes-fuzz (event-id uint) (amount uint)) + (begin + (unwrap! (stake-yes event-id amount) (ok false)) + (ok true) + ) +) + +;; Property: NO staking input validation +(define-public (test-stake-no-fuzz (event-id uint) (amount uint)) + (begin + (unwrap! (stake-no event-id amount) (ok false)) + (ok true) + ) +) + +;; Property: Event resolution input validation (admin only - may fail) +(define-public (test-resolve-event-fuzz (event-id uint) (winner bool)) + (begin + (unwrap! (resolve-event event-id winner) (ok false)) + (ok true) + ) +) + +;; Property: Claim rewards input validation +(define-public (test-claim-fuzz (event-id uint)) + (begin + (unwrap! (claim event-id) (ok false)) + (ok true) + ) +) + +;; Property: Create listing input validation +(define-public (test-create-listing-fuzz (points uint) (price-stx uint)) + (begin + (unwrap! (create-listing points price-stx) (ok false)) + (ok true) + ) +) + +;; Property: Buy listing input validation +(define-public (test-buy-listing-fuzz (listing-id uint) (points-to-buy uint)) + (begin + (unwrap! (buy-listing listing-id points-to-buy) (ok false)) + (ok true) + ) +) + +;; Property: Cancel listing input validation +(define-public (test-cancel-listing-fuzz (listing-id uint)) + (begin + (unwrap! (cancel-listing listing-id) (ok false)) + (ok true) + ) +) + +;; Property: Withdraw protocol fees input validation (admin only - may fail) +(define-public (test-withdraw-protocol-fees-fuzz (amount uint)) + (begin + (unwrap! (withdraw-protocol-fees amount) (ok false)) + (ok true) + ) +) + +;; Property: Create guild input validation +(define-public (test-create-guild-fuzz (guild-id uint) (name (string-ascii 50))) + (begin + (unwrap! (create-guild guild-id name) (ok false)) + (ok true) + ) +) + +;; Property: Join guild input validation +(define-public (test-join-guild-fuzz (guild-id uint)) + (begin + (unwrap! (join-guild guild-id) (ok false)) + (ok true) + ) +) + +;; Property: Leave guild input validation +(define-public (test-leave-guild-fuzz (guild-id uint)) + (begin + (unwrap! (leave-guild guild-id) (ok false)) + (ok true) + ) +) + +;; Property: Deposit to guild input validation +(define-public (test-deposit-to-guild-fuzz (guild-id uint) (amount uint)) + (begin + (unwrap! (deposit-to-guild guild-id amount) (ok false)) + (ok true) + ) +) + +;; Property: Withdraw from guild input validation +(define-public (test-withdraw-from-guild-fuzz (guild-id uint) (amount uint)) + (begin + (unwrap! (withdraw-from-guild guild-id amount) (ok false)) + (ok true) + ) +) + +;; Property: Guild stake YES input validation +(define-public (test-guild-stake-yes-fuzz (guild-id uint) (event-id uint) (amount uint)) + (begin + (unwrap! (guild-stake-yes guild-id event-id amount) (ok false)) + (ok true) + ) +) + +;; Property: Guild stake NO input validation +(define-public (test-guild-stake-no-fuzz (guild-id uint) (event-id uint) (amount uint)) + (begin + (unwrap! (guild-stake-no guild-id event-id amount) (ok false)) + (ok true) + ) +) + +;; Property: Guild claim input validation +(define-public (test-guild-claim-fuzz (guild-id uint) (event-id uint)) + (begin + (unwrap! (guild-claim guild-id event-id) (ok false)) + (ok true) + ) +) + +;; ============================================================================= +;; HELPER FUNCTIONS FOR STATE SETUP +;; ============================================================================= +;; These helpers allow Rendezvous to set up state needed for property tests + +;; Helper: Register user (sets up user state for other tests) +(define-public (test-register-helper (username (string-ascii 50))) + (let ((register-result (register username))) + (ok true) + ) +) + +;; Helper: Create event (admin only - may fail if not admin) +(define-public (test-create-event-helper (event-id uint) (metadata (string-ascii 200))) + (let ((create-result (create-event event-id metadata))) + (ok true) + ) +) + +;; Helper: Create guild (sets up guild state for other tests) +(define-public (test-create-guild-helper (guild-id uint) (name (string-ascii 50))) + (let ((create-result (create-guild guild-id name))) + (ok true) + ) +) + +;; Helper: Resolve event (admin only - may fail if not admin) +(define-public (test-resolve-event-helper (event-id uint) (winner bool)) + (let ((resolve-result (resolve-event event-id winner))) + (ok true) + ) +) + +;; ============================================================================= +;; PROPERTY TESTS WITH PRECONDITION CHECKING +;; ============================================================================= + +;; Property: Staking YES should deduct points from user and add to event pool +(define-public (test-stake-yes-property (event-id uint) (amount uint)) + (if (or + ;; Precondition 1: amount must be > 0 + (is-eq amount u0) + ;; Precondition 2: user must be registered (have points) + (is-none (map-get? user-points tx-sender)) + ;; Precondition 3: user must have enough points + (< (default-to u0 (map-get? user-points tx-sender)) amount) + ;; Precondition 4: event must exist and be open + (is-none (map-get? events event-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-points (default-to u0 (map-get? user-points tx-sender))) + (initial-yes-pool (get yes-pool event)) + ) + (if (is-eq (get status event) "open") + (begin + (unwrap! (stake-yes event-id amount) (ok false)) + (let ( + (final-points (default-to u0 (map-get? user-points tx-sender))) + (final-yes-pool (get yes-pool (unwrap! (map-get? events event-id) (ok false)))) + ) + ;; Verify property: points deducted and pool increased + (asserts! (is-eq final-points (- initial-points amount)) + (err u999) + ) + (asserts! (is-eq final-yes-pool (+ initial-yes-pool amount)) + (err u998) + ) + (ok true) + ) + ) + (ok false) ;; Event not open - discard + ) + ) + ) +) + +;; Property: Staking NO should deduct points from user and add to event pool +(define-public (test-stake-no-property (event-id uint) (amount uint)) + (if (or + ;; Precondition 1: amount must be > 0 + (is-eq amount u0) + ;; Precondition 2: user must be registered (have points) + (is-none (map-get? user-points tx-sender)) + ;; Precondition 3: user must have enough points + (< (default-to u0 (map-get? user-points tx-sender)) amount) + ;; Precondition 4: event must exist and be open + (is-none (map-get? events event-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-points (default-to u0 (map-get? user-points tx-sender))) + (initial-no-pool (get no-pool event)) + ) + (if (is-eq (get status event) "open") + (begin + (unwrap! (stake-no event-id amount) (ok false)) + (let ( + (final-points (default-to u0 (map-get? user-points tx-sender))) + (final-no-pool (get no-pool (unwrap! (map-get? events event-id) (ok false)))) + ) + ;; Verify property: points deducted and pool increased + (asserts! (is-eq final-points (- initial-points amount)) + (err u999) + ) + (asserts! (is-eq final-no-pool (+ initial-no-pool amount)) + (err u998) + ) + (ok true) + ) + ) + (ok false) ;; Event not open - discard + ) + ) + ) +) + +;; Property: Claiming should increase user points when winning +(define-public (test-claim-property (event-id uint)) + (if (or + ;; Precondition 1: event must exist + (is-none (map-get? events event-id)) + ;; Precondition 2: user must be registered + (is-none (map-get? user-points tx-sender)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-points (default-to u0 (map-get? user-points tx-sender))) + ) + (if (is-eq (get status event) "resolved") + (match (get winner event) + winner + (begin + ;; Check if user has a stake in the winning side + (match (if winner + (map-get? yes-stakes { event-id: event-id, user: tx-sender }) + (map-get? no-stakes { event-id: event-id, user: tx-sender }) + ) + stake + (if (> stake u0) + (begin + (unwrap! (claim event-id) (ok false)) + (let ((final-points (default-to u0 (map-get? user-points tx-sender)))) + ;; Verify property: points increased + (asserts! (>= final-points initial-points) + (err u997) + ) + (ok true) + ) + ) + (ok false) ;; No stake - discard + ) + (ok false) ;; No stake found - discard + ) + ) + (ok false) ;; Winner not set - discard + ) + (ok false) ;; Event not resolved - discard + ) + ) + ) +) + +;; Property: Creating listing should lock points and deduct from user +(define-public (test-create-listing-property (points uint) (price-stx uint)) + (if (or + ;; Precondition 1: points must be > 0 + (is-eq points u0) + ;; Precondition 2: price must be > 0 + (is-eq price-stx u0) + ;; Precondition 3: user must be registered + (is-none (map-get? user-points tx-sender)) + ;; Precondition 4: user must have enough points + (< (default-to u0 (map-get? user-points tx-sender)) points) + ;; Precondition 5: user must have earned >= 10,000 points + (< (default-to u0 (map-get? earned-points tx-sender)) u10000) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ((initial-points (default-to u0 (map-get? user-points tx-sender)))) + (unwrap! (create-listing points price-stx) (ok false)) + (let ((final-points (default-to u0 (map-get? user-points tx-sender)))) + ;; Verify property: points deducted + (asserts! (is-eq final-points (- initial-points points)) + (err u996) + ) + (ok true) + ) + ) + ) +) + +;; Property: Buying listing should transfer points to buyer +(define-public (test-buy-listing-property (listing-id uint) (points-to-buy uint)) + (if (or + ;; Precondition 1: points-to-buy must be > 0 + (is-eq points-to-buy u0) + ;; Precondition 2: listing must exist + (is-none (map-get? listings listing-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (listing (unwrap! (map-get? listings listing-id) (ok false))) + (initial-buyer-points (default-to u0 (map-get? user-points tx-sender))) + ) + (if (and + (get active listing) + (>= (get points listing) points-to-buy) + ) + (begin + (unwrap! (buy-listing listing-id points-to-buy) (ok false)) + (let ((final-buyer-points (default-to u0 (map-get? user-points tx-sender)))) + ;; Verify property: buyer received points + (asserts! (is-eq final-buyer-points (+ initial-buyer-points points-to-buy)) + (err u995) + ) + (ok true) + ) + ) + (ok false) ;; Listing not active or insufficient points - discard + ) + ) + ) +) + +;; Property: Canceling listing should return points to seller +(define-public (test-cancel-listing-property (listing-id uint)) + (if (or + ;; Precondition 1: listing must exist + (is-none (map-get? listings listing-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (listing (unwrap! (map-get? listings listing-id) (ok false))) + (initial-seller-points (default-to u0 (map-get? user-points (get seller listing)))) + (listing-points (get points listing)) + ) + (if (and + (get active listing) + (is-eq tx-sender (get seller listing)) + ) + (begin + (unwrap! (cancel-listing listing-id) (ok false)) + (let ((final-seller-points (default-to u0 (map-get? user-points (get seller listing))))) + ;; Verify property: seller got points back + (asserts! (is-eq final-seller-points (+ initial-seller-points listing-points)) + (err u994) + ) + (ok true) + ) + ) + (ok false) ;; Listing not active or not seller - discard + ) + ) + ) +) + +;; Property: Joining guild should add user as member +(define-public (test-join-guild-property (guild-id uint)) + (if (or + ;; Precondition 1: guild must exist + (is-none (map-get? guilds guild-id)) + ;; Precondition 2: user must not already be a member + (is-some (is-guild-member guild-id tx-sender)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (begin + (unwrap! (join-guild guild-id) (ok false)) + ;; Verify property: user is now a member + (match (is-guild-member guild-id tx-sender) + member-status + (if member-status + (ok true) + (ok false) ;; Member status is false + ) + (ok false) ;; Should not be none + ) + ) + ) +) + +;; Property: Depositing to guild should transfer points from user to guild +(define-public (test-deposit-to-guild-property (guild-id uint) (amount uint)) + (if (or + ;; Precondition 1: amount must be > 0 + (is-eq amount u0) + ;; Precondition 2: guild must exist + (is-none (map-get? guilds guild-id)) + ;; Precondition 3: user must be a member + (is-none (is-guild-member guild-id tx-sender)) + ;; Precondition 4: user must be registered + (is-none (map-get? user-points tx-sender)) + ;; Precondition 5: user must have enough points + (< (default-to u0 (map-get? user-points tx-sender)) amount) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (guild (unwrap! (map-get? guilds guild-id) (ok false))) + (initial-user-points (default-to u0 (map-get? user-points tx-sender))) + (initial-guild-points (get total-points guild)) + ) + (unwrap! (deposit-to-guild guild-id amount) (ok false)) + (let ( + (final-user-points (default-to u0 (map-get? user-points tx-sender))) + (final-guild-points (get total-points (unwrap! (map-get? guilds guild-id) (ok false)))) + ) + ;; Verify property: points transferred + (asserts! (is-eq final-user-points (- initial-user-points amount)) + (err u991) + ) + (asserts! (is-eq final-guild-points (+ initial-guild-points amount)) + (err u990) + ) + (ok true) + ) + ) + ) +) + +;; Property: Withdrawing from guild should transfer points from guild to user +(define-public (test-withdraw-from-guild-property (guild-id uint) (amount uint)) + (if (or + ;; Precondition 1: amount must be > 0 + (is-eq amount u0) + ;; Precondition 2: guild must exist + (is-none (map-get? guilds guild-id)) + ;; Precondition 3: user must be a member + (is-none (is-guild-member guild-id tx-sender)) + ;; Precondition 4: user must have deposits + (is-none (map-get? guild-deposits { guild-id: guild-id, user: tx-sender })) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (guild (unwrap! (map-get? guilds guild-id) (ok false))) + (user-deposit (unwrap! (map-get? guild-deposits { guild-id: guild-id, user: tx-sender }) (ok false))) + (initial-guild-points (get total-points guild)) + (initial-user-points (default-to u0 (map-get? user-points tx-sender))) + ) + (if (and + (>= user-deposit amount) + (>= initial-guild-points amount) + ) + (begin + (unwrap! (withdraw-from-guild guild-id amount) (ok false)) + (let ( + (final-guild-points (get total-points (unwrap! (map-get? guilds guild-id) (ok false)))) + (final-user-points (default-to u0 (map-get? user-points tx-sender))) + ) + ;; Verify property: points transferred + (asserts! (is-eq final-user-points (+ initial-user-points amount)) + (err u989) + ) + (asserts! (is-eq final-guild-points (- initial-guild-points amount)) + (err u988) + ) + (ok true) + ) + ) + (ok false) ;; Insufficient deposits or guild points - discard + ) + ) + ) +) + +;; Property: Guild staking YES should deduct from guild pool and add to event pool +(define-public (test-guild-stake-yes-property (guild-id uint) (event-id uint) (amount uint)) + (if (or + ;; Precondition 1: amount must be > 0 + (is-eq amount u0) + ;; Precondition 2: guild must exist + (is-none (map-get? guilds guild-id)) + ;; Precondition 3: user must be a member + (is-none (is-guild-member guild-id tx-sender)) + ;; Precondition 4: guild must have enough points + (< (get total-points (unwrap! (map-get? guilds guild-id) (ok false))) amount) + ;; Precondition 5: event must exist and be open + (is-none (map-get? events event-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (guild (unwrap! (map-get? guilds guild-id) (ok false))) + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-guild-points (get total-points guild)) + (initial-yes-pool (get yes-pool event)) + ) + (if (is-eq (get status event) "open") + (begin + (unwrap! (guild-stake-yes guild-id event-id amount) (ok false)) + (let ( + (final-guild-points (get total-points (unwrap! (map-get? guilds guild-id) (ok false)))) + (final-yes-pool (get yes-pool (unwrap! (map-get? events event-id) (ok false)))) + ) + ;; Verify property: guild points deducted and event pool increased + (asserts! (is-eq final-guild-points (- initial-guild-points amount)) + (err u987) + ) + (asserts! (is-eq final-yes-pool (+ initial-yes-pool amount)) + (err u986) + ) + (ok true) + ) + ) + (ok false) ;; Event not open - discard + ) + ) + ) +) + +;; Property: Guild staking NO should deduct from guild pool and add to event pool +(define-public (test-guild-stake-no-property (guild-id uint) (event-id uint) (amount uint)) + (if (or + ;; Precondition 1: amount must be > 0 + (is-eq amount u0) + ;; Precondition 2: guild must exist + (is-none (map-get? guilds guild-id)) + ;; Precondition 3: user must be a member + (is-none (is-guild-member guild-id tx-sender)) + ;; Precondition 4: guild must have enough points + (< (get total-points (unwrap! (map-get? guilds guild-id) (ok false))) amount) + ;; Precondition 5: event must exist and be open + (is-none (map-get? events event-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (guild (unwrap! (map-get? guilds guild-id) (ok false))) + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-guild-points (get total-points guild)) + (initial-no-pool (get no-pool event)) + ) + (if (is-eq (get status event) "open") + (begin + (unwrap! (guild-stake-no guild-id event-id amount) (ok false)) + (let ( + (final-guild-points (get total-points (unwrap! (map-get? guilds guild-id) (ok false)))) + (final-no-pool (get no-pool (unwrap! (map-get? events event-id) (ok false)))) + ) + ;; Verify property: guild points deducted and event pool increased + (asserts! (is-eq final-guild-points (- initial-guild-points amount)) + (err u985) + ) + (asserts! (is-eq final-no-pool (+ initial-no-pool amount)) + (err u984) + ) + (ok true) + ) + ) + (ok false) ;; Event not open - discard + ) + ) + ) +) + +;; Property: Guild claiming should increase guild points when winning +(define-public (test-guild-claim-property (guild-id uint) (event-id uint)) + (if (or + ;; Precondition 1: event must exist + (is-none (map-get? events event-id)) + ;; Precondition 2: guild must exist + (is-none (map-get? guilds guild-id)) + ;; Precondition 3: user must be a member + (is-none (is-guild-member guild-id tx-sender)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (event (unwrap! (map-get? events event-id) (ok false))) + (guild (unwrap! (map-get? guilds guild-id) (ok false))) + (initial-guild-points (get total-points guild)) + ) + (if (is-eq (get status event) "resolved") + (match (get winner event) + winner + (begin + ;; Check if guild has a stake in the winning side + (match (if winner + (map-get? guild-yes-stakes { guild-id: guild-id, event-id: event-id }) + (map-get? guild-no-stakes { guild-id: guild-id, event-id: event-id }) + ) + stake + (if (> stake u0) + (begin + (unwrap! (guild-claim guild-id event-id) (ok false)) + (let ((final-guild-points (get total-points (unwrap! (map-get? guilds guild-id) (ok false))))) + ;; Verify property: guild points increased + (asserts! (>= final-guild-points initial-guild-points) + (err u983) + ) + (ok true) + ) + ) + (ok false) ;; No stake - discard + ) + (ok false) ;; No stake found - discard + ) + ) + (ok false) ;; Winner not set - discard + ) + (ok false) ;; Event not resolved - discard + ) + ) + ) +) + +;; Property: Leaving guild should remove user as member (only if no deposits) +(define-public (test-leave-guild-property (guild-id uint)) + (if (or + ;; Precondition 1: guild must exist + (is-none (map-get? guilds guild-id)) + ;; Precondition 2: user must be a member + (is-none (is-guild-member guild-id tx-sender)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ((user-deposit (default-to u0 (map-get? guild-deposits { guild-id: guild-id, user: tx-sender })))) + ;; Can only leave if deposits are 0 + (if (is-eq user-deposit u0) + (begin + (unwrap! (leave-guild guild-id) (ok false)) + ;; Verify property: user is no longer a member + (match (is-guild-member guild-id tx-sender) + member-status + (if member-status + (ok false) ;; Still a member - test failed + (ok true) ;; Not a member - success + ) + (ok true) ;; Not a member (none) - success + ) + ) + (ok false) ;; Has deposits - discard (must withdraw first) + ) + ) + ) +) + +;; ============================================================================= +;; EDGE CASE TESTS +;; ============================================================================= + +;; Edge Case: Claiming when losing should clear stake but return 0 reward +(define-public (test-claim-losing-property (event-id uint)) + (if (or + ;; Precondition 1: event must exist + (is-none (map-get? events event-id)) + ;; Precondition 2: user must be registered + (is-none (map-get? user-points tx-sender)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-points (default-to u0 (map-get? user-points tx-sender))) + ) + (if (is-eq (get status event) "resolved") + (match (get winner event) + winner + (begin + ;; Check if user has a stake in the LOSING side + (match (if winner + (map-get? no-stakes { event-id: event-id, user: tx-sender }) + (map-get? yes-stakes { event-id: event-id, user: tx-sender }) + ) + stake + (if (> stake u0) + (begin + (unwrap! (claim event-id) (ok false)) + (let ((final-points (default-to u0 (map-get? user-points tx-sender)))) + ;; Verify property: points unchanged (no reward for losing) + (asserts! (is-eq final-points initial-points) + (err u982) + ) + ;; Verify stake was cleared + (match (if winner + (map-get? no-stakes { event-id: event-id, user: tx-sender }) + (map-get? yes-stakes { event-id: event-id, user: tx-sender }) + ) + remaining-stake + (begin + (asserts! (is-eq remaining-stake u0) (err u981)) + (ok true) + ) + (ok true) ;; Stake cleared (none) + ) + ) + ) + (ok false) ;; No losing stake - discard + ) + (ok false) ;; No stake found - discard + ) + ) + (ok false) ;; Winner not set - discard + ) + (ok false) ;; Event not resolved - discard + ) + ) + ) +) + +;; Edge Case: Partial purchase of listing should update listing correctly +(define-public (test-buy-listing-partial-property (listing-id uint) (points-to-buy uint)) + (if (or + ;; Precondition 1: points-to-buy must be > 0 + (is-eq points-to-buy u0) + ;; Precondition 2: listing must exist + (is-none (map-get? listings listing-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (listing (unwrap! (map-get? listings listing-id) (ok false))) + (initial-listing-points (get points listing)) + (initial-listing-price (get price-stx listing)) + ) + (if (and + (get active listing) + (> initial-listing-points points-to-buy) ;; Must be partial purchase + (>= initial-listing-points points-to-buy) + ) + (begin + (unwrap! (buy-listing listing-id points-to-buy) (ok false)) + (let ((updated-listing (unwrap! (map-get? listings listing-id) (ok false)))) + ;; Verify property: listing still active with reduced points + (asserts! (get active updated-listing) + (err u980) + ) + (asserts! (is-eq (get points updated-listing) (- initial-listing-points points-to-buy)) + (err u979) + ) + (ok true) + ) + ) + (ok false) ;; Not a partial purchase or invalid - discard + ) + ) + ) +) + +;; Edge Case: Multiple stakes on same event should accumulate +(define-public (test-stake-yes-accumulate-property (event-id uint) (amount1 uint) (amount2 uint)) + (if (or + ;; Precondition 1: amounts must be > 0 + (is-eq amount1 u0) + (is-eq amount2 u0) + ;; Precondition 2: user must be registered + (is-none (map-get? user-points tx-sender)) + ;; Precondition 3: user must have enough points for both stakes + (< (default-to u0 (map-get? user-points tx-sender)) (+ amount1 amount2)) + ;; Precondition 4: event must exist and be open + (is-none (map-get? events event-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-points (default-to u0 (map-get? user-points tx-sender))) + (initial-yes-pool (get yes-pool event)) + ) + (if (is-eq (get status event) "open") + (begin + ;; First stake + (unwrap! (stake-yes event-id amount1) (ok false)) + ;; Second stake + (unwrap! (stake-yes event-id amount2) (ok false)) + (let ( + (final-points (default-to u0 (map-get? user-points tx-sender))) + (final-yes-pool (get yes-pool (unwrap! (map-get? events event-id) (ok false)))) + (total-stake (unwrap! (map-get? yes-stakes { event-id: event-id, user: tx-sender }) (ok false))) + ) + ;; Verify property: points deducted correctly, pool increased, stake accumulated + (asserts! (is-eq final-points (- initial-points (+ amount1 amount2))) + (err u978) + ) + (asserts! (is-eq final-yes-pool (+ initial-yes-pool (+ amount1 amount2))) + (err u977) + ) + (asserts! (is-eq total-stake (+ amount1 amount2)) + (err u976) + ) + (ok true) + ) + ) + (ok false) ;; Event not open - discard + ) + ) + ) +) + +;; ============================================================================= +;; ENHANCED COVERAGE TESTS +;; ============================================================================= + +;; Enhanced: Create listing should add listing fee to treasury (10 STX) +(define-public (test-create-listing-fee-property (points uint) (price-stx uint)) + (if (or + ;; Precondition 1: points must be > 0 + (is-eq points u0) + ;; Precondition 2: price must be > 0 + (is-eq price-stx u0) + ;; Precondition 3: user must be registered + (is-none (map-get? user-points tx-sender)) + ;; Precondition 4: user must have enough points + (< (default-to u0 (map-get? user-points tx-sender)) points) + ;; Precondition 5: user must have earned >= 10,000 points + (< (default-to u0 (map-get? earned-points tx-sender)) u10000) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ((initial-treasury (var-get protocol-treasury))) + (unwrap! (create-listing points price-stx) (ok false)) + (let ((final-treasury (var-get protocol-treasury))) + ;; Verify property: listing fee (10 STX = 10,000,000 micro-STX) added to treasury + (asserts! (is-eq final-treasury (+ initial-treasury u10000000)) + (err u975) + ) + (ok true) + ) + ) + ) +) + +;; Enhanced: Buy listing should calculate and add protocol fee to treasury (2%) +(define-public (test-buy-listing-protocol-fee-property (listing-id uint) (points-to-buy uint)) + (if (or + ;; Precondition 1: points-to-buy must be > 0 + (is-eq points-to-buy u0) + ;; Precondition 2: listing must exist + (is-none (map-get? listings listing-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (listing (unwrap! (map-get? listings listing-id) (ok false))) + (initial-treasury (var-get protocol-treasury)) + ) + (if (and + (get active listing) + (>= (get points listing) points-to-buy) + ) + (let ( + (total-price (get price-stx listing)) + (total-points (get points listing)) + (price-per-point (/ total-price total-points)) + (actual-price-stx (* price-per-point points-to-buy)) + (expected-protocol-fee (/ (* actual-price-stx u200) u10000)) + ) + (unwrap! (buy-listing listing-id points-to-buy) (ok false)) + (let ((final-treasury (var-get protocol-treasury))) + ;; Verify property: protocol fee (2%) added to treasury + (asserts! (is-eq final-treasury (+ initial-treasury expected-protocol-fee)) + (err u973) + ) + (ok true) + ) + ) + (ok false) ;; Listing not active or insufficient points - discard + ) + ) + ) +) + +;; Enhanced: Username uniqueness should be enforced +(define-public (test-register-username-uniqueness-property (username (string-ascii 50))) + (if (or + ;; Precondition 1: username must not be empty (basic check) + (is-eq (len username) u0) + ;; Precondition 2: user must not already be registered + (is-some (map-get? user-points tx-sender)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (begin + ;; First registration should succeed + (unwrap! (register username) (ok false)) + ;; Verify username is stored and tracked for uniqueness + (match (get-username tx-sender) + stored-username + (begin + (asserts! (is-eq stored-username username) (err u971)) + ;; Verify username is tracked in usernames map for uniqueness + (match (map-get? usernames username) + existing-user + (begin + (asserts! (is-eq existing-user tx-sender) (err u960)) + (ok true) + ) + (ok false) ;; Username not tracked + ) + ) + (ok false) ;; Username not stored + ) + ) + ) +) + +;; Enhanced: Resolve event should transition state from open to resolved +(define-public (test-resolve-event-property (event-id uint) (winner bool)) + (if (or + ;; Precondition 1: event must exist + (is-none (map-get? events event-id)) + ;; Precondition 2: caller must be admin (will discard if not) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ((event (unwrap! (map-get? events event-id) (ok false)))) + (if (is-eq (get status event) "open") + (begin + (unwrap! (resolve-event event-id winner) (ok false)) + (let ((resolved-event (unwrap! (map-get? events event-id) (ok false)))) + ;; Verify property: status changed to resolved and winner set + (asserts! (is-eq (get status resolved-event) "resolved") + (err u970) + ) + (match (get winner resolved-event) + winner-value + (begin + (asserts! (is-eq winner-value winner) (err u969)) + (ok true) + ) + (ok false) ;; Winner not set + ) + ) + ) + (ok false) ;; Event not open - discard + ) + ) + ) +) + +;; Enhanced: Withdraw protocol fees should decrease treasury balance +(define-public (test-withdraw-protocol-fees-property (amount uint)) + (if (or + ;; Precondition 1: amount must be > 0 + (is-eq amount u0) + ;; Precondition 2: caller must be admin (will discard if not) + ;; Precondition 3: treasury must have enough balance + (< (var-get protocol-treasury) amount) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ((initial-treasury (var-get protocol-treasury))) + (unwrap! (withdraw-protocol-fees amount) (ok false)) + (let ((final-treasury (var-get protocol-treasury))) + ;; Verify property: treasury decreased by withdrawal amount + (asserts! (is-eq final-treasury (- initial-treasury amount)) + (err u968) + ) + (ok true) + ) + ) + ) +) + +;; Enhanced: Claiming twice should fail after first claim (stake cleared) +(define-public (test-claim-twice-property (event-id uint)) + (if (or + ;; Precondition 1: event must exist + (is-none (map-get? events event-id)) + ;; Precondition 2: user must be registered + (is-none (map-get? user-points tx-sender)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ((event (unwrap! (map-get? events event-id) (ok false)))) + (if (is-eq (get status event) "resolved") + (match (get winner event) + winner + (begin + ;; Check if user has a stake in the winning side + (match (if winner + (map-get? yes-stakes { event-id: event-id, user: tx-sender }) + (map-get? no-stakes { event-id: event-id, user: tx-sender }) + ) + stake + (if (> stake u0) + (begin + ;; First claim should succeed + (unwrap! (claim event-id) (ok false)) + ;; Verify stake was cleared (set to 0) - this proves second claim would fail + (match (if winner + (map-get? yes-stakes { event-id: event-id, user: tx-sender }) + (map-get? no-stakes { event-id: event-id, user: tx-sender }) + ) + remaining-stake + (begin + ;; Stake should be 0 after claiming (proves second claim would fail) + (asserts! (is-eq remaining-stake u0) (err u961)) + (ok true) + ) + (ok true) ;; Stake cleared (none) - second claim would fail + ) + ) + (ok false) ;; No stake - discard + ) + (ok false) ;; No stake found - discard + ) + ) + (ok false) ;; Winner not set - discard + ) + (ok false) ;; Event not resolved - discard + ) + ) + ) +) + +;; Enhanced: Boundary condition - very large amounts +(define-public (test-stake-yes-boundary-property (event-id uint) (amount uint)) + (if (or + ;; Precondition 1: amount must be > 0 + (is-eq amount u0) + ;; Precondition 2: user must be registered + (is-none (map-get? user-points tx-sender)) + ;; Precondition 3: user must have enough points + (< (default-to u0 (map-get? user-points tx-sender)) amount) + ;; Precondition 4: event must exist and be open + (is-none (map-get? events event-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test - same as regular stake-yes but tests with boundary values + (let ( + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-points (default-to u0 (map-get? user-points tx-sender))) + (initial-yes-pool (get yes-pool event)) + ) + (if (is-eq (get status event) "open") + (begin + (unwrap! (stake-yes event-id amount) (ok false)) + (let ( + (final-points (default-to u0 (map-get? user-points tx-sender))) + (final-yes-pool (get yes-pool (unwrap! (map-get? events event-id) (ok false)))) + ) + ;; Verify property: points deducted and pool increased (even with large amounts) + (asserts! (is-eq final-points (- initial-points amount)) + (err u966) + ) + (asserts! (is-eq final-yes-pool (+ initial-yes-pool amount)) + (err u965) + ) + ;; Verify no overflow occurred (final should be >= initial for pool) + (asserts! (>= final-yes-pool initial-yes-pool) + (err u964) + ) + (ok true) + ) + ) + (ok false) ;; Event not open - discard + ) + ) + ) +) + +;; Enhanced: Boundary condition - minimum values (1 point) +(define-public (test-stake-yes-minimum-property (event-id uint)) + (if (or + ;; Precondition 1: user must be registered + (is-none (map-get? user-points tx-sender)) + ;; Precondition 2: user must have at least 1 point + (< (default-to u0 (map-get? user-points tx-sender)) u1) + ;; Precondition 3: event must exist and be open + (is-none (map-get? events event-id)) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test with minimum amount (1 point) + (let ( + (event (unwrap! (map-get? events event-id) (ok false))) + (initial-points (default-to u0 (map-get? user-points tx-sender))) + (initial-yes-pool (get yes-pool event)) + ) + (if (is-eq (get status event) "open") + (begin + (unwrap! (stake-yes event-id u1) (ok false)) + (let ( + (final-points (default-to u0 (map-get? user-points tx-sender))) + (final-yes-pool (get yes-pool (unwrap! (map-get? events event-id) (ok false)))) + ) + ;; Verify property: minimum stake works correctly + (asserts! (is-eq final-points (- initial-points u1)) + (err u963) + ) + (asserts! (is-eq final-yes-pool (+ initial-yes-pool u1)) + (err u962) + ) + (ok true) +) + ) + (ok false) ;; Event not open - discard + ) + ) + ) +) + + + diff --git a/package-lock.json b/package-lock.json index d70a2d7..8995478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@stacks/clarinet-sdk": "^3.10.0", + "@stacks/rendezvous": "^0.12.0", "@stacks/transactions": "^7.0.6", "chokidar-cli": "^3.0.0", "vitest": "^4.0.0", @@ -786,6 +787,23 @@ "cross-fetch": "^3.1.5" } }, + "node_modules/@stacks/rendezvous": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@stacks/rendezvous/-/rendezvous-0.12.0.tgz", + "integrity": "sha512-zequrpWDwVKOoPFocVRkWm5ZymNzYimO5UJ3MhMp2A5OQuXmvE7LCslTLMOfryDblKi+aVOkoex1idcAD3pmqg==", + "license": "GPL-3.0-only", + "dependencies": { + "@stacks/clarinet-sdk": "^3.9.1", + "@stacks/transactions": "^7.2.0", + "ansicolor": "^2.0.3", + "fast-check": "^4.3.0", + "toml": "^3.0.0", + "yaml": "^2.8.1" + }, + "bin": { + "rv": "dist/app.js" + } + }, "node_modules/@stacks/transactions": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-7.3.0.tgz", @@ -956,6 +974,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansicolor": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ansicolor/-/ansicolor-2.0.3.tgz", + "integrity": "sha512-pzusTqk9VHrjgMCcTPDTTvfJfx6Q3+L5tQ6yKC8Diexmoit4YROTFIkxFvRTNL9y5s0Q8HrSrgerCD5bIC+Kiw==", + "license": "Unlicense" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1320,6 +1344,28 @@ "node": ">=12.0.0" } }, + "node_modules/fast-check": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.4.0.tgz", + "integrity": "sha512-s87BFAp8YaWYOBXjbTxeotaOhmA4hPYAyk9gBTFxdab25P6eAlqrryUvVMA2qd9bT/0Xq+YNJGtoVhJd/BxI4g==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^7.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1664,6 +1710,22 @@ "node": ">= 6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1884,6 +1946,12 @@ "node": ">=8.0" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -2156,6 +2224,21 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", diff --git a/package.json b/package.json index 234ab0b..1cfd19f 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,3 @@ - { "name": "Roxy-tests", "version": "1.0.0", @@ -14,6 +13,7 @@ "license": "ISC", "dependencies": { "@stacks/clarinet-sdk": "^3.10.0", + "@stacks/rendezvous": "^0.12.0", "@stacks/transactions": "^7.0.6", "chokidar-cli": "^3.0.0", "vitest": "^4.0.0", diff --git a/tests/roxy.test.ts b/tests/roxy.test.ts index 95d7c31..4d5e3e3 100644 --- a/tests/roxy.test.ts +++ b/tests/roxy.test.ts @@ -75,6 +75,11 @@ function accumulateEarnedPoints(user: string, opponent: string, startEventId: nu } describe("Roxy Contract Tests", () => { + it("ensures the contract is deployed", () => { + const contractSource = simnet.getContractSource("roxy"); + expect(contractSource).toBeDefined(); + }); + describe("register", () => { it("should register a new user successfully", () => { const { result } = simnet.callPublicFn(