From 5037938232ea748baa25d3fb63603abda084dc9e Mon Sep 17 00:00:00 2001 From: samuel1-ona Date: Mon, 15 Dec 2025 09:38:17 +0100 Subject: [PATCH 1/3] Updated the test to verify data variables and maps --- tests/roxy.test.ts | 306 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 280 insertions(+), 26 deletions(-) diff --git a/tests/roxy.test.ts b/tests/roxy.test.ts index 4d5e3e3..2eb5345 100644 --- a/tests/roxy.test.ts +++ b/tests/roxy.test.ts @@ -90,7 +90,23 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify user points + // Verify user points via map + const userPoints = simnet.getMapEntry(contractName, "user-points", Cl.principal(address1)); + expect(userPoints).toBeSome(Cl.uint(1000)); + + // Verify earned points via map + const earnedPoints = simnet.getMapEntry(contractName, "earned-points", Cl.principal(address1)); + expect(earnedPoints).toBeSome(Cl.uint(0)); + + // Verify user name via map + const userName = simnet.getMapEntry(contractName, "user-names", Cl.principal(address1)); + expect(userName).toBeSome(Cl.stringAscii("alice")); + + // Verify username uniqueness map + const usernameMapping = simnet.getMapEntry(contractName, "usernames", Cl.stringAscii("alice")); + expect(usernameMapping).toBeSome(Cl.principal(address1)); + + // Verify user points via read-only function const { result: pointsResult } = simnet.callReadOnlyFn( contractName, "get-user-points", @@ -99,7 +115,7 @@ describe("Roxy Contract Tests", () => { ); expect(pointsResult).toBeOk(Cl.some(Cl.uint(1000))); - // Verify earned points + // Verify earned points via read-only function const { result: earnedResult } = simnet.callReadOnlyFn( contractName, "get-earned-points", @@ -108,7 +124,7 @@ describe("Roxy Contract Tests", () => { ); expect(earnedResult).toBeOk(Cl.some(Cl.uint(0))); - // Verify username + // Verify username via read-only function const { result: usernameResult } = simnet.callReadOnlyFn( contractName, "get-username", @@ -155,7 +171,20 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify event + // Verify event via map + const event = simnet.getMapEntry(contractName, "events", Cl.uint(1)); + expect(event).toBeSome( + Cl.tuple({ + "yes-pool": Cl.uint(0), + "no-pool": Cl.uint(0), + status: Cl.stringAscii("open"), + winner: Cl.none(), + creator: Cl.principal(deployer), + metadata: Cl.stringAscii("Will Bitcoin reach $100k?"), + }) + ); + + // Verify event via read-only function const { result: eventResult } = simnet.callReadOnlyFn( contractName, "get-event", @@ -223,7 +252,36 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify stake + // Verify stake via map + const stakeKey = Cl.tuple({ + "event-id": Cl.uint(1), + user: Cl.principal(address1), + }); + const stake = simnet.getMapEntry(contractName, "yes-stakes", stakeKey); + expect(stake).toBeSome(Cl.uint(100)); + + // Verify user points reduced via map + const userPoints = simnet.getMapEntry(contractName, "user-points", Cl.principal(address1)); + expect(userPoints).toBeSome(Cl.uint(900)); // 1000 - 100 + + // Verify event pool updated via map + const event = simnet.getMapEntry(contractName, "events", Cl.uint(1)); + expect(event).toBeSome( + Cl.tuple({ + "yes-pool": Cl.uint(100), + "no-pool": Cl.uint(0), + status: Cl.stringAscii("open"), + winner: Cl.none(), + creator: Cl.principal(deployer), + metadata: Cl.stringAscii("Test event"), + }) + ); + + // Verify total YES stakes via data var + const totalYesStakes = simnet.getDataVar(contractName, "total-yes-stakes"); + expect(totalYesStakes).toBeUint(100); + + // Verify stake via read-only function const { result: stakeResult } = simnet.callReadOnlyFn( contractName, "get-yes-stake", @@ -232,7 +290,7 @@ describe("Roxy Contract Tests", () => { ); expect(stakeResult).toBeSome(Cl.uint(100)); - // Verify user points reduced + // Verify user points reduced via read-only function const { result: pointsResult } = simnet.callReadOnlyFn( contractName, "get-user-points", @@ -241,7 +299,7 @@ describe("Roxy Contract Tests", () => { ); expect(pointsResult).toBeOk(Cl.some(Cl.uint(900))); // 1000 - 100 - // Verify event pool updated + // Verify event pool updated via read-only function const { result: eventResult } = simnet.callReadOnlyFn( contractName, "get-event", @@ -259,7 +317,7 @@ describe("Roxy Contract Tests", () => { }) ); - // Verify total YES stakes + // Verify total YES stakes via read-only function const { result: totalYesResult } = simnet.callReadOnlyFn( contractName, "get-total-yes-stakes", @@ -361,7 +419,19 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify stake + // Verify stake via map + const stakeKey = Cl.tuple({ + "event-id": Cl.uint(1), + user: Cl.principal(address1), + }); + const stake = simnet.getMapEntry(contractName, "no-stakes", stakeKey); + expect(stake).toBeSome(Cl.uint(100)); + + // Verify total NO stakes via data var + const totalNoStakes = simnet.getDataVar(contractName, "total-no-stakes"); + expect(totalNoStakes).toBeUint(100); + + // Verify stake via read-only function const { result: stakeResult } = simnet.callReadOnlyFn( contractName, "get-no-stake", @@ -370,7 +440,7 @@ describe("Roxy Contract Tests", () => { ); expect(stakeResult).toBeSome(Cl.uint(100)); - // Verify total NO stakes + // Verify total NO stakes via read-only function const { result: totalNoResult } = simnet.callReadOnlyFn( contractName, "get-total-no-stakes", @@ -412,7 +482,20 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify event resolved + // Verify event resolved via map + const event = simnet.getMapEntry(contractName, "events", Cl.uint(1)); + expect(event).toBeSome( + Cl.tuple({ + "yes-pool": Cl.uint(0), + "no-pool": Cl.uint(0), + status: Cl.stringAscii("resolved"), + winner: Cl.some(Cl.bool(true)), + creator: Cl.principal(deployer), + metadata: Cl.stringAscii("Test event"), + }) + ); + + // Verify event resolved via read-only function const { result: eventResult } = simnet.callReadOnlyFn( contractName, "get-event", @@ -506,7 +589,35 @@ describe("Roxy Contract Tests", () => { expect(result.value).toStrictEqual(Cl.uint(300)); } - // Verify user points increased + // Verify user points increased via map + const userPoints = simnet.getMapEntry(contractName, "user-points", Cl.principal(address1)); + expect(userPoints).toBeSome(Cl.uint(1200)); // 1000 - 100 + 300 + + // Verify earned points increased via map + const earnedPoints = simnet.getMapEntry(contractName, "earned-points", Cl.principal(address1)); + expect(earnedPoints).toBeSome(Cl.uint(300)); + + // Verify stake cleared via map + const stakeKey = Cl.tuple({ + "event-id": Cl.uint(1), + user: Cl.principal(address1), + }); + const stake = simnet.getMapEntry(contractName, "yes-stakes", stakeKey); + expect(stake).toBeSome(Cl.uint(0)); + + // Verify user stats updated via map + const userStats = simnet.getMapEntry(contractName, "user-stats", Cl.principal(address1)); + expect(userStats).toBeSome( + Cl.tuple({ + "total-predictions": Cl.uint(1), + wins: Cl.uint(1), + losses: Cl.uint(0), + "total-points-earned": Cl.uint(300), + "win-rate": Cl.uint(10000), // 100% + }) + ); + + // Verify user points increased via read-only function const { result: pointsResult } = simnet.callReadOnlyFn( contractName, "get-user-points", @@ -515,7 +626,7 @@ describe("Roxy Contract Tests", () => { ); expect(pointsResult).toBeOk(Cl.some(Cl.uint(1200))); // 1000 - 100 + 300 - // Verify earned points increased + // Verify earned points increased via read-only function const { result: earnedResult } = simnet.callReadOnlyFn( contractName, "get-earned-points", @@ -524,7 +635,7 @@ describe("Roxy Contract Tests", () => { ); expect(earnedResult).toBeOk(Cl.some(Cl.uint(300))); - // Verify stake cleared + // Verify stake cleared via read-only function const { result: stakeResult } = simnet.callReadOnlyFn( contractName, "get-yes-stake", @@ -533,7 +644,7 @@ describe("Roxy Contract Tests", () => { ); expect(stakeResult).toBeSome(Cl.uint(0)); - // Verify user stats updated + // Verify user stats updated via read-only function const { result: statsResult } = simnet.callReadOnlyFn( contractName, "get-user-stats", @@ -684,7 +795,22 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.uint(1)); - // Verify listing + // Verify listing via map + const listing = simnet.getMapEntry(contractName, "listings", Cl.uint(1)); + expect(listing).toBeSome( + Cl.tuple({ + seller: Cl.principal(address1), + points: Cl.uint(500), + "price-stx": Cl.uint(1000000), + active: Cl.bool(true), + }) + ); + + // Verify next-listing-id data var + const nextListingId = simnet.getDataVar(contractName, "next-listing-id"); + expect(nextListingId).toBeUint(2); // Should be incremented to 2 + + // Verify listing via read-only function const { result: listingResult } = simnet.callReadOnlyFn( contractName, "get-listing", @@ -780,7 +906,29 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify listing deactivated + // Verify listing deactivated via map + const listing = simnet.getMapEntry(contractName, "listings", Cl.uint(1)); + expect(listing).toBeSome( + Cl.tuple({ + seller: Cl.principal(address1), + points: Cl.uint(0), + "price-stx": Cl.uint(0), + active: Cl.bool(false), + }) + ); + + // Verify Bob got points via map (starts with 1000 from registration, gets 500 from purchase = 1500) + const bobPoints = simnet.getMapEntry(contractName, "user-points", Cl.principal(address2)); + expect(bobPoints).toBeSome(Cl.uint(1500)); + + // Verify protocol treasury increased via data var (should have listing fee + protocol fee) + const protocolTreasury = simnet.getDataVar(contractName, "protocol-treasury"); + expect(protocolTreasury.type).toBe("uint"); + if (protocolTreasury.type === "uint") { + expect(protocolTreasury.value).toBeGreaterThan(0); + } + + // Verify listing deactivated via read-only function const { result: listingResult } = simnet.callReadOnlyFn( contractName, "get-listing", @@ -796,7 +944,7 @@ describe("Roxy Contract Tests", () => { }) ); - // Verify Bob got points (starts with 1000 from registration, gets 500 from purchase = 1500) + // Verify Bob got points via read-only function const { result: bobPointsResult } = simnet.callReadOnlyFn( contractName, "get-user-points", @@ -1112,7 +1260,33 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify guild + // Verify guild via map + const guild = simnet.getMapEntry(contractName, "guilds", Cl.uint(1)); + expect(guild).toBeSome( + Cl.tuple({ + creator: Cl.principal(address1), + name: Cl.stringAscii("Test Guild"), + "total-points": Cl.uint(0), + "member-count": Cl.uint(1), + }) + ); + + // Verify creator is member via map + const memberKey = Cl.tuple({ + "guild-id": Cl.uint(1), + user: Cl.principal(address1), + }); + const isMember = simnet.getMapEntry(contractName, "guild-members", memberKey); + expect(isMember).toBeSome(Cl.bool(true)); + + // Verify next-guild-id data var + const nextGuildId = simnet.getDataVar(contractName, "next-guild-id"); + expect(nextGuildId.type).toBe("uint"); + if (nextGuildId.type === "uint") { + expect(nextGuildId.value).toBeGreaterThanOrEqual(1); + } + + // Verify guild via read-only function const { result: guildResult } = simnet.callReadOnlyFn( contractName, "get-guild", @@ -1128,7 +1302,7 @@ describe("Roxy Contract Tests", () => { }) ); - // Verify creator is member + // Verify creator is member via read-only function const { result: memberResult } = simnet.callReadOnlyFn( contractName, "is-guild-member", @@ -1312,7 +1486,26 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify deposit + // Verify deposit via map + const depositKey = Cl.tuple({ + "guild-id": Cl.uint(1), + user: Cl.principal(address2), + }); + const deposit = simnet.getMapEntry(contractName, "guild-deposits", depositKey); + expect(deposit).toBeSome(Cl.uint(100)); + + // Verify guild points increased via map + const guild = simnet.getMapEntry(contractName, "guilds", Cl.uint(1)); + expect(guild).toBeSome( + Cl.tuple({ + creator: Cl.principal(address1), + name: Cl.stringAscii("Test Guild"), + "total-points": Cl.uint(100), + "member-count": Cl.uint(2), + }) + ); + + // Verify deposit via read-only function const { result: depositResult } = simnet.callReadOnlyFn( contractName, "get-guild-deposit", @@ -1321,7 +1514,7 @@ describe("Roxy Contract Tests", () => { ); expect(depositResult).toBeSome(Cl.uint(100)); - // Verify guild points increased + // Verify guild points increased via read-only function const { result: guildResult } = simnet.callReadOnlyFn( contractName, "get-guild", @@ -1473,7 +1666,30 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify guild stake + // Verify guild stake via map + const guildStakeKey = Cl.tuple({ + "guild-id": Cl.uint(1), + "event-id": Cl.uint(1), + }); + const guildStake = simnet.getMapEntry(contractName, "guild-yes-stakes", guildStakeKey); + expect(guildStake).toBeSome(Cl.uint(100)); + + // Verify guild points reduced via map + const guild = simnet.getMapEntry(contractName, "guilds", Cl.uint(1)); + expect(guild).toBeSome( + Cl.tuple({ + creator: Cl.principal(address1), + name: Cl.stringAscii("Test Guild"), + "total-points": Cl.uint(400), // 500 - 100 + "member-count": Cl.uint(2), + }) + ); + + // Verify total guild YES stakes via data var + const totalGuildYesStakes = simnet.getDataVar(contractName, "total-guild-yes-stakes"); + expect(totalGuildYesStakes).toBeUint(100); + + // Verify guild stake via read-only function const { result: stakeResult } = simnet.callReadOnlyFn( contractName, "get-guild-yes-stake", @@ -1482,7 +1698,7 @@ describe("Roxy Contract Tests", () => { ); expect(stakeResult).toBeSome(Cl.uint(100)); - // Verify guild points reduced + // Verify guild points reduced via read-only function const { result: guildResult } = simnet.callReadOnlyFn( contractName, "get-guild", @@ -1498,7 +1714,7 @@ describe("Roxy Contract Tests", () => { }) ); - // Verify total guild YES stakes + // Verify total guild YES stakes via read-only function const { result: totalResult } = simnet.callReadOnlyFn( contractName, "get-total-guild-yes-stakes", @@ -1580,7 +1796,19 @@ describe("Roxy Contract Tests", () => { ); expect(result).toBeOk(Cl.bool(true)); - // Verify total guild NO stakes + // Verify guild NO stake via map + const guildStakeKey = Cl.tuple({ + "guild-id": Cl.uint(1), + "event-id": Cl.uint(1), + }); + const guildStake = simnet.getMapEntry(contractName, "guild-no-stakes", guildStakeKey); + expect(guildStake).toBeSome(Cl.uint(100)); + + // Verify total guild NO stakes via data var + const totalGuildNoStakes = simnet.getDataVar(contractName, "total-guild-no-stakes"); + expect(totalGuildNoStakes).toBeUint(100); + + // Verify total guild NO stakes via read-only function const { result: totalResult } = simnet.callReadOnlyFn( contractName, "get-total-guild-no-stakes", @@ -1706,6 +1934,11 @@ describe("Roxy Contract Tests", () => { describe("get-admin", () => { it("should return admin address", () => { + // Verify admin via data var + const admin = simnet.getDataVar(contractName, "admin"); + expect(admin).toStrictEqual(Cl.principal(deployer)); + + // Verify admin via read-only function const { result } = simnet.callReadOnlyFn( contractName, "get-admin", @@ -1722,6 +1955,27 @@ describe("Roxy Contract Tests", () => { }); it("should return transaction log", () => { + // Verify transaction log via map + const transactionLog = simnet.getMapEntry(contractName, "transaction-logs", Cl.uint(1)); + expect(transactionLog).toBeSome( + Cl.tuple({ + action: Cl.stringAscii("register"), + user: Cl.principal(address1), + "event-id": Cl.none(), + "listing-id": Cl.none(), + amount: Cl.some(Cl.uint(1000)), + metadata: Cl.stringAscii("alice"), + }) + ); + + // Verify next-log-id data var + const nextLogId = simnet.getDataVar(contractName, "next-log-id"); + expect(nextLogId.type).toBe("uint"); + if (nextLogId.type === "uint") { + expect(nextLogId.value).toBeGreaterThanOrEqual(1); + } + + // Verify transaction log via read-only function const { result } = simnet.callReadOnlyFn( contractName, "get-transaction-log", From 7766807d3442befdad64aee2100ab74c22b384ab Mon Sep 17 00:00:00 2001 From: samuel1-ona Date: Mon, 15 Dec 2025 10:07:06 +0100 Subject: [PATCH 2/3] Updated rendezvous:Added invariant test --- contracts/roxy.tests.clar | 392 +++++++++++++++++++++++++++ deployments/default.simnet-plan.yaml | 2 +- 2 files changed, 393 insertions(+), 1 deletion(-) diff --git a/contracts/roxy.tests.clar b/contracts/roxy.tests.clar index aad1bbb..656ec80 100644 --- a/contracts/roxy.tests.clar +++ b/contracts/roxy.tests.clar @@ -1180,5 +1180,397 @@ ) ) +;; ============================================================================= +;; INVARIANT TESTS +;; ============================================================================= +;; These invariants should always hold true regardless of state transitions. +;; Rendezvous will randomly execute public functions and check these invariants. + +;; Invariant: Protocol treasury should never be negative +(define-read-only (invariant-treasury-non-negative) + (>= (var-get protocol-treasury) u0) +) + +;; Invariant: Total YES stakes counter should be non-negative +(define-read-only (invariant-total-yes-stakes-non-negative) + (>= (var-get total-yes-stakes) u0) +) + +;; Invariant: Total NO stakes counter should be non-negative +(define-read-only (invariant-total-no-stakes-non-negative) + (>= (var-get total-no-stakes) u0) +) + +;; Invariant: Total guild YES stakes counter should be non-negative +(define-read-only (invariant-total-guild-yes-stakes-non-negative) + (>= (var-get total-guild-yes-stakes) u0) +) + +;; Invariant: Total guild NO stakes counter should be non-negative +(define-read-only (invariant-total-guild-no-stakes-non-negative) + (>= (var-get total-guild-no-stakes) u0) +) + +;; Invariant: Next event ID should be positive +(define-read-only (invariant-next-event-id-positive) + (> (var-get next-event-id) u0) +) + +;; Invariant: Next listing ID should be positive +(define-read-only (invariant-next-listing-id-positive) + (> (var-get next-listing-id) u0) +) + +;; Invariant: Next guild ID should be positive +(define-read-only (invariant-next-guild-id-positive) + (> (var-get next-guild-id) u0) +) + +;; Invariant: Admin should always be set +;; This ensures the admin variable is properly initialized +(define-read-only (invariant-admin-exists) + true ;; Admin is always set via define-data-var, so this always holds +) + +;; Invariant: If a user is registered, their points should be non-negative +;; This checks the current transaction sender's points if they're registered +(define-read-only (invariant-user-points-non-negative) + (match (map-get? user-points tx-sender) + points + (>= points u0) + true ;; If user not registered, invariant holds (nothing to check) + ) +) + +;; Invariant: If a user is registered, their earned points should be non-negative +(define-read-only (invariant-earned-points-non-negative) + (match (map-get? earned-points tx-sender) + earned + (>= earned u0) + true ;; If user not registered, invariant holds + ) +) + +;; Invariant: Username consistency - if user has username, it should map correctly +(define-read-only (invariant-username-consistency) + (match (map-get? user-names tx-sender) + username + (match (map-get? usernames username) + mapped-user + (is-eq mapped-user tx-sender) ;; Username should map back to this user + false ;; Username exists but not in usernames map - inconsistency + ) + true ;; No username set - invariant holds + ) +) + +;; Invariant: If user has username, it should be unique (reverse mapping exists) +(define-read-only (invariant-username-uniqueness) + (match (map-get? user-names tx-sender) + username + (is-some (map-get? usernames username)) ;; Username should exist in uniqueness map + true ;; No username - invariant holds + ) +) + +;; Invariant: Event status should be valid (open, closed, or resolved) +;; This checks a specific event if it exists - we'll check event ID 1 as a sample +(define-read-only (invariant-event-status-valid (event-id uint)) + (match (map-get? events event-id) + event + (let ((status (get status event))) + (or + (is-eq status "open") + (is-eq status "closed") + (is-eq status "resolved") + ) + ) + true ;; Event doesn't exist - invariant holds + ) +) + +;; Invariant: Resolved events must have a winner set +(define-read-only (invariant-resolved-events-have-winner (event-id uint)) + (match (map-get? events event-id) + event + (if (is-eq (get status event) "resolved") + (is-some (get winner event)) ;; Resolved events must have winner + true ;; Not resolved - invariant holds + ) + true ;; Event doesn't exist - invariant holds + ) +) +;; Invariant: Event pools should be non-negative +(define-read-only (invariant-event-pools-non-negative (event-id uint)) + (match (map-get? events event-id) + event + (and + (>= (get yes-pool event) u0) + (>= (get no-pool event) u0) + ) + true ;; Event doesn't exist - invariant holds + ) +) +;; Invariant: User stakes should be non-negative +(define-read-only (invariant-user-stakes-non-negative (event-id uint)) + (and + (match (map-get? yes-stakes { event-id: event-id, user: tx-sender }) + stake + (>= stake u0) + true ;; No stake - invariant holds + ) + (match (map-get? no-stakes { event-id: event-id, user: tx-sender }) + stake + (>= stake u0) + true ;; No stake - invariant holds + ) + ) +) + +;; Invariant: Guild total points should be non-negative +(define-read-only (invariant-guild-points-non-negative (guild-id uint)) + (match (map-get? guilds guild-id) + guild + (>= (get total-points guild) u0) + true ;; Guild doesn't exist - invariant holds + ) +) + +;; Invariant: Guild member count should be positive if guild exists +(define-read-only (invariant-guild-member-count-positive (guild-id uint)) + (match (map-get? guilds guild-id) + guild + (> (get member-count guild) u0) ;; Guilds must have at least 1 member + true ;; Guild doesn't exist - invariant holds + ) +) + +;; Invariant: Guild deposits should be non-negative +(define-read-only (invariant-guild-deposits-non-negative (guild-id uint)) + (match (map-get? guild-deposits { guild-id: guild-id, user: tx-sender }) + deposit + (>= deposit u0) + true ;; No deposit - invariant holds + ) +) + +;; Invariant: Guild stakes should be non-negative +(define-read-only (invariant-guild-stakes-non-negative (guild-id uint) (event-id uint)) + (and + (match (map-get? guild-yes-stakes { guild-id: guild-id, event-id: event-id }) + stake + (>= stake u0) + true ;; No stake - invariant holds + ) + (match (map-get? guild-no-stakes { guild-id: guild-id, event-id: event-id }) + stake + (>= stake u0) + true ;; No stake - invariant holds + ) + ) +) + +;; Invariant: Listing points should be non-negative +(define-read-only (invariant-listing-points-non-negative (listing-id uint)) + (match (map-get? listings listing-id) + listing + (>= (get points listing) u0) + true ;; Listing doesn't exist - invariant holds + ) +) + +;; Invariant: Listing price should be non-negative +(define-read-only (invariant-listing-price-non-negative (listing-id uint)) + (match (map-get? listings listing-id) + listing + (>= (get price-stx listing) u0) + true ;; Listing doesn't exist - invariant holds + ) +) + +;; Invariant: If user is a guild member, they should have a deposit record +(define-read-only (invariant-guild-member-has-deposit (guild-id uint)) + (match (is-guild-member guild-id tx-sender) + is-member + (if is-member + (is-some (map-get? guild-deposits { guild-id: guild-id, user: tx-sender })) ;; Member should have deposit record + true ;; Not a member - invariant holds + ) + true ;; Not a member (none) - invariant holds + ) +) + +;; Invariant: Points conservation - user points + earned points should be consistent +;; (earned points should never exceed total points for registered users) +;; Note: This is a simplified check - full conservation would require summing all state +(define-read-only (invariant-points-consistency) + (match (map-get? user-points tx-sender) + user-pts + (match (map-get? earned-points tx-sender) + earned-pts + (>= user-pts u0) ;; User points should be non-negative + (>= user-pts u0) ;; If no earned points, user points should still be non-negative + ) + true ;; User not registered - invariant holds + ) +) + +;; Invariant: Using context - track that stake operations maintain pool consistency +;; This uses the Rendezvous context to ensure stake operations are balanced +(define-read-only (invariant-stake-operations-balanced) + (let + ( + (stake-yes-calls (match (map-get? context "stake-yes") + ctx-entry (get called ctx-entry) + u0 + )) + (stake-no-calls (match (map-get? context "stake-no") + ctx-entry (get called ctx-entry) + u0 + )) + (claim-calls (match (map-get? context "claim") + ctx-entry (get called ctx-entry) + u0 + )) + ) + ;; Basic sanity: if we've had stake operations, totals should be non-negative + ;; This invariant ensures that stake operations don't corrupt the global counters + (and + (>= (var-get total-yes-stakes) u0) + (>= (var-get total-no-stakes) u0) + ) + ) +) + +;; Invariant: Event pool consistency - pools should match sum of stakes +;; Note: This is a simplified version checking that pools are at least as large as tracked stakes +;; Full verification would require iterating all stakes, which isn't practical in Clarity +(define-read-only (invariant-event-pool-consistency (event-id uint)) + (match (map-get? events event-id) + event + (let + ( + (yes-pool (get yes-pool event)) + (no-pool (get no-pool event)) + ) + ;; Pools should be non-negative and pools should be >= 0 + ;; (Full consistency would require summing all individual stakes) + (and + (>= yes-pool u0) + (>= no-pool u0) + ) + ) + true ;; Event doesn't exist - invariant holds + ) +) + +;; Invariant: Guild points should match sum of deposits +;; Simplified check - verifies guild points are non-negative +;; Full verification would require iterating all member deposits +(define-read-only (invariant-guild-points-consistency (guild-id uint)) + (match (map-get? guilds guild-id) + guild + (let + ( + (total-pts (get total-points guild)) + ) + ;; Guild points should be non-negative + ;; (Full consistency would require summing all member deposits) + (>= total-pts u0) + ) + true ;; Guild doesn't exist - invariant holds + ) +) + +;; Invariant: Active listings should have positive points +(define-read-only (invariant-active-listing-has-points (listing-id uint)) + (match (map-get? listings listing-id) + listing + (if (get active listing) + (> (get points listing) u0) ;; Active listings must have points + true ;; Not active - invariant holds + ) + true ;; Listing doesn't exist - invariant holds + ) +) + +;; Invariant: Active listings should have positive price +(define-read-only (invariant-active-listing-has-price (listing-id uint)) + (match (map-get? listings listing-id) + listing + (if (get active listing) + (> (get price-stx listing) u0) ;; Active listings must have price + true ;; Not active - invariant holds + ) + true ;; Listing doesn't exist - invariant holds + ) +) + +;; Invariant: User stats should have consistent win rate calculation +;; Win rate should be between 0 and 10000 (0% to 100%) +(define-read-only (invariant-user-stats-win-rate-valid) + (match (map-get? user-stats tx-sender) + stats + (let + ( + (win-rate (get win-rate stats)) + ) + (and + (>= win-rate u0) + (<= win-rate u10000) ;; Win rate as percentage (0-10000 = 0%-100%) + ) + ) + true ;; No stats - invariant holds + ) +) + +;; Invariant: User stats wins + losses should not exceed total predictions +(define-read-only (invariant-user-stats-consistency) + (match (map-get? user-stats tx-sender) + stats + (let + ( + (total (get total-predictions stats)) + (wins (get wins stats)) + (losses (get losses stats)) + ) + (>= total (+ wins losses)) ;; Total should be at least wins + losses + ) + true ;; No stats - invariant holds + ) +) + +;; Invariant: Guild stats should have consistent win rate calculation +(define-read-only (invariant-guild-stats-win-rate-valid (guild-id uint)) + (match (map-get? guild-stats guild-id) + stats + (let + ( + (win-rate (get win-rate stats)) + ) + (and + (>= win-rate u0) + (<= win-rate u10000) ;; Win rate as percentage (0-10000 = 0%-100%) + ) + ) + true ;; No stats - invariant holds + ) +) + +;; Invariant: Guild stats wins + losses should not exceed total predictions +(define-read-only (invariant-guild-stats-consistency (guild-id uint)) + (match (map-get? guild-stats guild-id) + stats + (let + ( + (total (get total-predictions stats)) + (wins (get wins stats)) + (losses (get losses stats)) + ) + (>= total (+ wins losses)) ;; Total should be at least wins + losses + ) + true ;; No stats - invariant holds + ) +) diff --git a/deployments/default.simnet-plan.yaml b/deployments/default.simnet-plan.yaml index aeeae6a..0fece9f 100644 --- a/deployments/default.simnet-plan.yaml +++ b/deployments/default.simnet-plan.yaml @@ -68,4 +68,4 @@ plan: emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM path: contracts/roxy.clar clarity-version: 3 - epoch: "3.3" + epoch: "3.2" From 67ca60f86120adfdad0b1469c2874aaacdfe8162 Mon Sep 17 00:00:00 2001 From: samuel1-ona Date: Mon, 15 Dec 2025 10:11:14 +0100 Subject: [PATCH 3/3] Github workflow: Updated workflow to run invariant test --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c514fea..b498460 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,11 +34,12 @@ jobs: - name: Install dependencies run: npm ci - - name: Run fuzz tests - run: npx rv . roxy test - - + - name: Run property-based tests + run: npx rv . roxy test --runs=100 + - name: Run invariant tests + run: npx rv . roxy invariant --runs=100 + tests: runs-on: ubuntu-latest