From 9b7b5bd87408b7162341b2dc25ae120b158ee37e Mon Sep 17 00:00:00 2001 From: samuel1-ona Date: Wed, 17 Dec 2025 17:48:29 +0100 Subject: [PATCH 1/2] Added admin controlled mint point function and test cases --- contracts/roxy.clar | 142 ++++++++++++++++++++++++++ contracts/roxy.tests.clar | 145 +++++++++++++++++++++++++++ deployments/default.simnet-plan.yaml | 2 +- tests/roxy.test.ts | 140 ++++++++++++++++++++++++++ 4 files changed, 428 insertions(+), 1 deletion(-) diff --git a/contracts/roxy.clar b/contracts/roxy.clar index 0bbc16b..b5e96b9 100644 --- a/contracts/roxy.clar +++ b/contracts/roxy.clar @@ -53,6 +53,8 @@ (define-data-var total-no-stakes uint u0) ;; Total NO stakes across all events (define-data-var total-guild-yes-stakes uint u0) ;; Total guild YES stakes across all events (define-data-var total-guild-no-stakes uint u0) ;; Total guild NO stakes across all events +(define-data-var total-admin-minted-points uint u0) ;; Total points minted by admin +(define-constant ADMIN_POINT_PRICE u1000) ;; 1000 micro-STX per point (1 STX = 1000 points) ;; data maps ;; User Point System @@ -1615,6 +1617,135 @@ ) ) +;; ============================================================================ +;; 10b. mint-admin-points (points) +;; ============================================================================ +;; Purpose: Admin mints points to themselves for selling to users who need points. +;; +;; Details: +;; - Only callable by admin +;; - Mints points directly to admin's user-points balance +;; - Does NOT add to earned-points (won't affect leaderboard) +;; - No fee charged +;; +;; Parameters: +;; - points: uint - Number of points to mint +;; +;; Returns: +;; - (ok true) on success +;; - ERR-NOT-ADMIN if caller is not admin +;; - ERR-INVALID-AMOUNT if points <= 0 +;; ============================================================================ +(define-public (mint-admin-points (points uint)) + (let ( + (caller tx-sender) + (admin-principal (var-get admin)) + ) + (asserts! (is-eq caller admin-principal) ERR-NOT-ADMIN) + (asserts! (> points u0) ERR-INVALID-AMOUNT) + ;; Add points to admin's balance + (match (map-get? user-points admin-principal) + current-points + (map-set user-points admin-principal (+ current-points points)) + (map-set user-points admin-principal points) + ) + ;; Track total minted points + (var-set total-admin-minted-points (+ (var-get total-admin-minted-points) points)) + ;; Emit event + (print { + event: "admin-points-minted", + admin: admin-principal, + points: points, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "mint-admin-points", + user: admin-principal, + event-id: none, + listing-id: none, + amount: (some points), + metadata: "admin-points-minted", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok true) + ) +) + +;; ============================================================================ +;; 10c. buy-admin-points (points-to-buy) +;; ============================================================================ +;; Purpose: Users buy points directly from admin at a fixed rate, no fees. +;; +;; Details: +;; - Anyone can call this to buy points from admin +;; - Fixed price: 1 STX per 1000 points (1000 micro-STX per point) +;; - STX goes directly into the contract (protocol treasury) +;; - Points deducted from admin's balance, added to buyer's balance +;; - No protocol fee charged +;; - Does NOT add to buyer's earned-points (won't affect leaderboard) +;; +;; Parameters: +;; - points-to-buy: uint - Number of points to buy +;; +;; Returns: +;; - (ok true) on success +;; - ERR-INVALID-AMOUNT if points-to-buy <= 0 +;; - ERR-INSUFFICIENT-POINTS if admin doesn't have enough points +;; ============================================================================ + + +(define-public (buy-admin-points (points-to-buy uint)) + (let ( + (buyer tx-sender) + (admin-principal (var-get admin)) + (total-price (* points-to-buy ADMIN_POINT_PRICE)) + ) + (asserts! (> points-to-buy u0) ERR-INVALID-AMOUNT) + ;; Check admin has enough points + (match (map-get? user-points admin-principal) + admin-points + (begin + (asserts! (>= admin-points points-to-buy) ERR-INSUFFICIENT-POINTS) + ;; Transfer STX from buyer to contract (no fee, all goes to treasury) + (try! (stx-transfer? total-price buyer (as-contract tx-sender))) + ;; Add to protocol treasury + (var-set protocol-treasury (+ (var-get protocol-treasury) total-price)) + ;; Deduct points from admin + (map-set user-points admin-principal (- admin-points points-to-buy)) + ;; Add 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) + ) + ;; Emit event + (print { + event: "admin-points-bought", + buyer: buyer, + points: points-to-buy, + price-stx: total-price, + }) + ;; Log transaction + (let ((log-id (var-get next-log-id))) + (map-set transaction-logs log-id { + action: "buy-admin-points", + user: buyer, + event-id: none, + listing-id: none, + amount: (some points-to-buy), + metadata: "admin-points-bought", + }) + (var-set next-log-id (+ log-id u1)) + ) + (ok true) + ) + ERR-INSUFFICIENT-POINTS ;; Admin has no points + ) + ) +) + ;; ============================================================================ ;; 11. create-guild (guild-id, name) ;; ============================================================================ @@ -2988,6 +3119,17 @@ (ok (var-get protocol-treasury)) ) +;; ============================================================================ +;; 18b. get-total-admin-minted-points +;; ============================================================================ +;; Purpose: Get the total points minted by admin. +;; +;; Returns: (ok total-minted-points) +;; ============================================================================ +(define-read-only (get-total-admin-minted-points) + (ok (var-get total-admin-minted-points)) +) + ;; ============================================================================ ;; 19. get-admin ;; ============================================================================ diff --git a/contracts/roxy.tests.clar b/contracts/roxy.tests.clar index 656ec80..fd00efc 100644 --- a/contracts/roxy.tests.clar +++ b/contracts/roxy.tests.clar @@ -1180,6 +1180,146 @@ ) ) +;; ============================================================================= +;; ADMIN POINTS MINTING AND BUYING TESTS +;; ============================================================================= + +;; Property: Mint admin points input validation (admin only - may fail) +(define-public (test-mint-admin-points-fuzz (points uint)) + (begin + (unwrap! (mint-admin-points points) (ok false)) + (ok true) + ) +) + +;; Property: Buy admin points input validation +(define-public (test-buy-admin-points-fuzz (points-to-buy uint)) + (begin + (unwrap! (buy-admin-points points-to-buy) (ok false)) + (ok true) + ) +) + +;; Property: Mint admin points should increase admin's user-points but NOT earned-points +(define-public (test-mint-admin-points-property (points uint)) + (if (or + ;; Precondition 1: points must be > 0 + (is-eq points u0) + ;; Precondition 2: caller must be admin + (not (is-eq tx-sender (var-get admin))) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (admin-principal (var-get admin)) + (initial-user-points (default-to u0 (map-get? user-points admin-principal))) + (initial-earned-points (default-to u0 (map-get? earned-points admin-principal))) + (initial-total-minted (var-get total-admin-minted-points)) + ) + (unwrap! (mint-admin-points points) (ok false)) + (let ( + (final-user-points (default-to u0 (map-get? user-points admin-principal))) + (final-earned-points (default-to u0 (map-get? earned-points admin-principal))) + (final-total-minted (var-get total-admin-minted-points)) + ) + ;; Verify property: user-points increased by minted amount + (asserts! (is-eq final-user-points (+ initial-user-points points)) + (err u950) + ) + ;; Verify property: earned-points NOT increased (shouldn't affect leaderboard) + (asserts! (is-eq final-earned-points initial-earned-points) + (err u949) + ) + ;; Verify property: total-admin-minted-points increased + (asserts! (is-eq final-total-minted (+ initial-total-minted points)) + (err u948) + ) + (ok true) + ) + ) + ) +) + +;; Property: Buy admin points should transfer points from admin to buyer, NOT add to earned-points +(define-public (test-buy-admin-points-property (points-to-buy uint)) + (if (or + ;; Precondition 1: points-to-buy must be > 0 + (is-eq points-to-buy u0) + ;; Precondition 2: admin must have enough points + (< (default-to u0 (map-get? user-points (var-get admin))) points-to-buy) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ( + (admin-principal (var-get admin)) + (buyer tx-sender) + (initial-admin-points (default-to u0 (map-get? user-points admin-principal))) + (initial-buyer-points (default-to u0 (map-get? user-points buyer))) + (initial-buyer-earned (default-to u0 (map-get? earned-points buyer))) + (initial-treasury (var-get protocol-treasury)) + (expected-price (* points-to-buy u1000)) ;; ADMIN_POINT_PRICE = 1000 micro-STX per point + ) + (unwrap! (buy-admin-points points-to-buy) (ok false)) + (let ( + (final-admin-points (default-to u0 (map-get? user-points admin-principal))) + (final-buyer-points (default-to u0 (map-get? user-points buyer))) + (final-buyer-earned (default-to u0 (map-get? earned-points buyer))) + (final-treasury (var-get protocol-treasury)) + ) + ;; Verify property: admin points decreased + (asserts! (is-eq final-admin-points (- initial-admin-points points-to-buy)) + (err u947) + ) + ;; Verify property: buyer points increased + (asserts! (is-eq final-buyer-points (+ initial-buyer-points points-to-buy)) + (err u946) + ) + ;; Verify property: buyer earned-points NOT increased (shouldn't affect leaderboard) + (asserts! (is-eq final-buyer-earned initial-buyer-earned) + (err u945) + ) + ;; Verify property: treasury increased by payment amount + (asserts! (is-eq final-treasury (+ initial-treasury expected-price)) + (err u944) + ) + (ok true) + ) + ) + ) +) + +;; Property: Bought points should NOT allow selling (earned-points unchanged) +(define-public (test-buy-admin-points-no-sell-property (points-to-buy uint)) + (if (or + ;; Precondition 1: points-to-buy must be > 0 + (is-eq points-to-buy u0) + ;; Precondition 2: admin must have enough points + (< (default-to u0 (map-get? user-points (var-get admin))) points-to-buy) + ;; Precondition 3: buyer must NOT already have 10,000+ earned points + (>= (default-to u0 (map-get? earned-points tx-sender)) u10000) + ) + ;; Discard if preconditions aren't met + (ok false) + ;; Run the test + (let ((initial-earned (default-to u0 (map-get? earned-points tx-sender)))) + (unwrap! (buy-admin-points points-to-buy) (ok false)) + (let ((final-earned (default-to u0 (map-get? earned-points tx-sender)))) + ;; Verify property: earned-points unchanged after buying + (asserts! (is-eq final-earned initial-earned) + (err u943) + ) + ;; Verify property: user still cannot sell (earned < 10,000) + (asserts! (< final-earned u10000) + (err u942) + ) + (ok true) + ) + ) + ) +) + ;; ============================================================================= ;; INVARIANT TESTS ;; ============================================================================= @@ -1211,6 +1351,11 @@ (>= (var-get total-guild-no-stakes) u0) ) +;; Invariant: Total admin minted points should be non-negative +(define-read-only (invariant-total-admin-minted-points-non-negative) + (>= (var-get total-admin-minted-points) u0) +) + ;; Invariant: Next event ID should be positive (define-read-only (invariant-next-event-id-positive) (> (var-get next-event-id) u0) diff --git a/deployments/default.simnet-plan.yaml b/deployments/default.simnet-plan.yaml index 0fece9f..aeeae6a 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.2" + epoch: "3.3" diff --git a/tests/roxy.test.ts b/tests/roxy.test.ts index 2eb5345..8f8df07 100644 --- a/tests/roxy.test.ts +++ b/tests/roxy.test.ts @@ -1949,6 +1949,146 @@ describe("Roxy Contract Tests", () => { }); }); + describe("mint-admin-points", () => { + beforeEach(() => { + simnet.callPublicFn(contractName, "register", [Cl.stringAscii("admin")], deployer); + }); + + it("should mint points to admin successfully", () => { + const { result } = simnet.callPublicFn( + contractName, + "mint-admin-points", + [Cl.uint(5000)], + deployer + ); + expect(result).toBeOk(Cl.bool(true)); + + // Verify admin got points + const adminPoints = simnet.getMapEntry(contractName, "user-points", Cl.principal(deployer)); + expect(adminPoints).toBeSome(Cl.uint(6000)); // 1000 from register + 5000 minted + + // Verify earned-points NOT increased (shouldn't affect leaderboard) + const earnedPoints = simnet.getMapEntry(contractName, "earned-points", Cl.principal(deployer)); + expect(earnedPoints).toBeSome(Cl.uint(0)); + }); + + it("should fail if not admin", () => { + simnet.callPublicFn(contractName, "register", [Cl.stringAscii("alice")], address1); + const { result } = simnet.callPublicFn( + contractName, + "mint-admin-points", + [Cl.uint(5000)], + address1 + ); + expect(result).toBeErr(Cl.uint(2)); // ERR-NOT-ADMIN + }); + + it("should fail if points is 0", () => { + const { result } = simnet.callPublicFn( + contractName, + "mint-admin-points", + [Cl.uint(0)], + deployer + ); + expect(result).toBeErr(Cl.uint(4)); // ERR-INVALID-AMOUNT + }); + + it("should track total minted points via getter", () => { + // Mint 5000 points + simnet.callPublicFn(contractName, "mint-admin-points", [Cl.uint(5000)], deployer); + + // Check getter + const { result: result1 } = simnet.callReadOnlyFn( + contractName, + "get-total-admin-minted-points", + [], + deployer + ); + expect(result1).toBeOk(Cl.uint(5000)); + + // Mint another 3000 points + simnet.callPublicFn(contractName, "mint-admin-points", [Cl.uint(3000)], deployer); + + // Check getter again - should be cumulative + const { result: result2 } = simnet.callReadOnlyFn( + contractName, + "get-total-admin-minted-points", + [], + deployer + ); + expect(result2).toBeOk(Cl.uint(8000)); + }); + }); + + describe("buy-admin-points", () => { + beforeEach(() => { + simnet.callPublicFn(contractName, "register", [Cl.stringAscii("admin")], deployer); + simnet.callPublicFn(contractName, "register", [Cl.stringAscii("alice")], address1); + // Admin mints points to sell + simnet.callPublicFn(contractName, "mint-admin-points", [Cl.uint(10000)], deployer); + }); + + it("should buy points from admin successfully", () => { + const { result } = simnet.callPublicFn( + contractName, + "buy-admin-points", + [Cl.uint(1000)], // Buy 1000 points = 1 STX + address1 + ); + expect(result).toBeOk(Cl.bool(true)); + + // Verify buyer got points (1000 from register + 1000 bought) + const buyerPoints = simnet.getMapEntry(contractName, "user-points", Cl.principal(address1)); + expect(buyerPoints).toBeSome(Cl.uint(2000)); + + // Verify buyer's earned-points NOT increased (shouldn't affect leaderboard) + const earnedPoints = simnet.getMapEntry(contractName, "earned-points", Cl.principal(address1)); + expect(earnedPoints).toBeSome(Cl.uint(0)); + + // Verify admin's points decreased (1000 from register + 10000 minted - 1000 sold) + const adminPoints = simnet.getMapEntry(contractName, "user-points", Cl.principal(deployer)); + expect(adminPoints).toBeSome(Cl.uint(10000)); + + // Verify protocol treasury increased (1000 points * 1000 micro-STX = 1,000,000 micro-STX) + const treasury = simnet.getDataVar(contractName, "protocol-treasury"); + expect(treasury).toBeUint(1000000); + }); + + it("should fail if points-to-buy is 0", () => { + const { result } = simnet.callPublicFn( + contractName, + "buy-admin-points", + [Cl.uint(0)], + address1 + ); + expect(result).toBeErr(Cl.uint(4)); // ERR-INVALID-AMOUNT + }); + + it("should fail if admin has insufficient points", () => { + const { result } = simnet.callPublicFn( + contractName, + "buy-admin-points", + [Cl.uint(50000)], // More than admin has + address1 + ); + expect(result).toBeErr(Cl.uint(6)); // ERR-INSUFFICIENT-POINTS + }); + + it("should not add to buyer earned-points (leaderboard check)", () => { + // Buy points + simnet.callPublicFn(contractName, "buy-admin-points", [Cl.uint(5000)], address1); + + // Check can-sell returns false (earned-points should still be 0) + const { result } = simnet.callReadOnlyFn( + contractName, + "can-sell", + [Cl.principal(address1)], + address1 + ); + expect(result).toBeOk(Cl.bool(false)); // Can't sell because earned-points is 0 + }); + }); + describe("get-transaction-log", () => { beforeEach(() => { simnet.callPublicFn(contractName, "register", [Cl.stringAscii("alice")], address1); From c1f19c9bb2763d2e5c5e26b8f8196bdd31f1434a Mon Sep 17 00:00:00 2001 From: samuel1-ona Date: Wed, 17 Dec 2025 19:34:41 +0100 Subject: [PATCH 2/2] Added admin configuration system and test case --- contracts/roxy.clar | 167 ++++++++++++++++++++++++++++++++++++++++---- tests/roxy.test.ts | 164 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+), 14 deletions(-) diff --git a/contracts/roxy.clar b/contracts/roxy.clar index b5e96b9..ac23486 100644 --- a/contracts/roxy.clar +++ b/contracts/roxy.clar @@ -10,12 +10,15 @@ ;; ;; constants -(define-constant STARTING_POINTS u1000) -(define-constant MIN_EARNED_FOR_SELL u10000) -(define-constant LISTING_FEE u10000000) ;; 10 STX in micro-STX -(define-constant PROTOCOL_FEE_BPS u200) ;; 2% = 200 basis points (define-constant BPS_DENOMINATOR u10000) +;; configurable parameters (can be changed by admin) +(define-data-var starting-points uint u1000) +(define-data-var min-earned-for-sell uint u10000) +(define-data-var listing-fee uint u10000000) ;; 10 STX in micro-STX +(define-data-var protocol-fee-bps uint u200) ;; 2% = 200 basis points +(define-data-var admin-point-price uint u1000) ;; 1000 micro-STX per point (1 STX = 1000 points) + ;; error constants (define-constant ERR-USER-ALREADY-REGISTERED (err u1)) (define-constant ERR-NOT-ADMIN (err u2)) @@ -54,7 +57,6 @@ (define-data-var total-guild-yes-stakes uint u0) ;; Total guild YES stakes across all events (define-data-var total-guild-no-stakes uint u0) ;; Total guild NO stakes across all events (define-data-var total-admin-minted-points uint u0) ;; Total points minted by admin -(define-constant ADMIN_POINT_PRICE u1000) ;; 1000 micro-STX per point (1 STX = 1000 points) ;; data maps ;; User Point System @@ -239,7 +241,7 @@ existing-user ERR-USERNAME-TAKEN ;; Username already taken (begin - (map-set user-points user STARTING_POINTS) + (map-set user-points user (var-get 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 @@ -248,7 +250,7 @@ event: "user-registered", user: user, username: username, - points: STARTING_POINTS, + points: (var-get starting-points), }) ;; Log transaction (let ((log-id (var-get next-log-id))) @@ -257,7 +259,7 @@ user: user, event-id: none, listing-id: none, - amount: (some STARTING_POINTS), + amount: (some (var-get starting-points)), metadata: username, }) (var-set next-log-id (+ log-id u1)) @@ -1302,16 +1304,16 @@ (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 + (asserts! (>= earned (var-get min-earned-for-sell)) ERR-INSUFFICIENT-EARNED-POINTS) ;; Must have earned minimum 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))) + (try! (stx-transfer? (var-get listing-fee) seller (as-contract tx-sender))) ;; Add listing fee to protocol treasury (var-set protocol-treasury - (+ (var-get protocol-treasury) LISTING_FEE) + (+ (var-get protocol-treasury) (var-get listing-fee)) ) ;; Lock seller's points by deducting them (map-set user-points seller (- current-points points)) @@ -1416,7 +1418,7 @@ (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)) + (protocol-fee (/ (* actual-price-stx (var-get 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)) @@ -1700,7 +1702,7 @@ (let ( (buyer tx-sender) (admin-principal (var-get admin)) - (total-price (* points-to-buy ADMIN_POINT_PRICE)) + (total-price (* points-to-buy (var-get admin-point-price))) ) (asserts! (> points-to-buy u0) ERR-INVALID-AMOUNT) ;; Check admin has enough points @@ -2979,7 +2981,7 @@ ;; ============================================================================ (define-read-only (can-sell (user principal)) (match (map-get? earned-points user) - earned (ok (>= earned MIN_EARNED_FOR_SELL)) + earned (ok (>= earned (var-get min-earned-for-sell))) (ok false) ) ) @@ -3146,6 +3148,143 @@ (ok (var-get admin)) ) +;; ============================================================================ +;; 19b. transfer-admin (new-admin) +;; ============================================================================ +;; Purpose: Transfer admin role to a new principal. +;; +;; Parameters: +;; - new-admin: principal - The new admin's principal address +;; +;; Returns: +;; - (ok true) on success +;; - ERR-NOT-ADMIN if caller is not admin +;; ============================================================================ +(define-public (transfer-admin (new-admin principal)) + (begin + (asserts! (is-eq tx-sender (var-get admin)) ERR-NOT-ADMIN) + (var-set admin new-admin) + (print { event: "admin-transferred", old-admin: tx-sender, new-admin: new-admin }) + (ok true) + ) +) + +;; ============================================================================ +;; 19c. set-min-earned-for-sell (amount) +;; ============================================================================ +;; Purpose: Set minimum earned points required to create listings (admin only). +;; +;; Parameters: +;; - amount: uint - New minimum earned points threshold +;; +;; Returns: +;; - (ok true) on success +;; - ERR-NOT-ADMIN if caller is not admin +;; ============================================================================ +(define-public (set-min-earned-for-sell (amount uint)) + (begin + (asserts! (is-eq tx-sender (var-get admin)) ERR-NOT-ADMIN) + (var-set min-earned-for-sell amount) + (print { event: "min-earned-for-sell-updated", new-value: amount }) + (ok true) + ) +) + +;; ============================================================================ +;; 19d. set-listing-fee (amount) +;; ============================================================================ +;; Purpose: Set the listing fee in micro-STX (admin only). +;; +;; Parameters: +;; - amount: uint - New listing fee in micro-STX +;; +;; Returns: +;; - (ok true) on success +;; - ERR-NOT-ADMIN if caller is not admin +;; ============================================================================ +(define-public (set-listing-fee (amount uint)) + (begin + (asserts! (is-eq tx-sender (var-get admin)) ERR-NOT-ADMIN) + (var-set listing-fee amount) + (print { event: "listing-fee-updated", new-value: amount }) + (ok true) + ) +) + +;; ============================================================================ +;; 19e. set-protocol-fee-bps (bps) +;; ============================================================================ +;; Purpose: Set the protocol fee in basis points (admin only). +;; +;; Parameters: +;; - bps: uint - New protocol fee in basis points (100 = 1%) +;; +;; Returns: +;; - (ok true) on success +;; - ERR-NOT-ADMIN if caller is not admin +;; ============================================================================ +(define-public (set-protocol-fee-bps (bps uint)) + (begin + (asserts! (is-eq tx-sender (var-get admin)) ERR-NOT-ADMIN) + (asserts! (<= bps u1000) ERR-INVALID-AMOUNT) ;; Max 10% + (var-set protocol-fee-bps bps) + (print { event: "protocol-fee-bps-updated", new-value: bps }) + (ok true) + ) +) + +;; ============================================================================ +;; 19f. set-admin-point-price (price) +;; ============================================================================ +;; Purpose: Set the price per point for admin point sales (admin only). +;; +;; Parameters: +;; - price: uint - New price in micro-STX per point +;; +;; Returns: +;; - (ok true) on success +;; - ERR-NOT-ADMIN if caller is not admin +;; ============================================================================ +(define-public (set-admin-point-price (price uint)) + (begin + (asserts! (is-eq tx-sender (var-get admin)) ERR-NOT-ADMIN) + (asserts! (> price u0) ERR-INVALID-AMOUNT) + (var-set admin-point-price price) + (print { event: "admin-point-price-updated", new-value: price }) + (ok true) + ) +) + +;; ============================================================================ +;; 19g. set-starting-points (amount) +;; ============================================================================ +;; Purpose: Set starting points for new users (admin only). +;; +;; Parameters: +;; - amount: uint - New starting points amount +;; +;; Returns: +;; - (ok true) on success +;; - ERR-NOT-ADMIN if caller is not admin +;; ============================================================================ +(define-public (set-starting-points (amount uint)) + (begin + (asserts! (is-eq tx-sender (var-get admin)) ERR-NOT-ADMIN) + (var-set starting-points amount) + (print { event: "starting-points-updated", new-value: amount }) + (ok true) + ) +) + +;; ============================================================================ +;; Read-only getters for configurable parameters +;; ============================================================================ +(define-read-only (get-min-earned-for-sell) (var-get min-earned-for-sell)) +(define-read-only (get-listing-fee) (var-get listing-fee)) +(define-read-only (get-protocol-fee-bps) (var-get protocol-fee-bps)) +(define-read-only (get-admin-point-price) (var-get admin-point-price)) +(define-read-only (get-starting-points) (var-get starting-points)) + ;; ============================================================================ ;; 21. get-guild (guild-id) ;; ============================================================================ diff --git a/tests/roxy.test.ts b/tests/roxy.test.ts index 8f8df07..5ca0a9e 100644 --- a/tests/roxy.test.ts +++ b/tests/roxy.test.ts @@ -2089,6 +2089,170 @@ describe("Roxy Contract Tests", () => { }); }); + describe("admin configuration functions", () => { + beforeEach(() => { + simnet.callPublicFn(contractName, "register", [Cl.stringAscii("admin")], deployer); + simnet.callPublicFn(contractName, "register", [Cl.stringAscii("alice")], address1); + }); + + it("should transfer admin successfully", () => { + const { result } = simnet.callPublicFn( + contractName, + "transfer-admin", + [Cl.principal(address1)], + deployer + ); + expect(result).toBeOk(Cl.bool(true)); + + // Verify new admin + const { result: adminResult } = simnet.callReadOnlyFn( + contractName, + "get-admin", + [], + address1 + ); + expect(adminResult).toBeOk(Cl.principal(address1)); + }); + + it("should fail transfer-admin if not admin", () => { + const { result } = simnet.callPublicFn( + contractName, + "transfer-admin", + [Cl.principal(address2)], + address1 + ); + expect(result).toBeErr(Cl.uint(2)); // ERR-NOT-ADMIN + }); + + it("should set min-earned-for-sell successfully", () => { + const { result } = simnet.callPublicFn( + contractName, + "set-min-earned-for-sell", + [Cl.uint(5000)], + deployer + ); + expect(result).toBeOk(Cl.bool(true)); + + const { result: getResult } = simnet.callReadOnlyFn( + contractName, + "get-min-earned-for-sell", + [], + address1 + ); + expect(getResult).toStrictEqual(Cl.uint(5000)); + }); + + it("should set listing-fee successfully", () => { + const { result } = simnet.callPublicFn( + contractName, + "set-listing-fee", + [Cl.uint(5000000)], // 5 STX + deployer + ); + expect(result).toBeOk(Cl.bool(true)); + + const { result: getResult } = simnet.callReadOnlyFn( + contractName, + "get-listing-fee", + [], + address1 + ); + expect(getResult).toStrictEqual(Cl.uint(5000000)); + }); + + it("should set protocol-fee-bps successfully", () => { + const { result } = simnet.callPublicFn( + contractName, + "set-protocol-fee-bps", + [Cl.uint(500)], // 5% + deployer + ); + expect(result).toBeOk(Cl.bool(true)); + + const { result: getResult } = simnet.callReadOnlyFn( + contractName, + "get-protocol-fee-bps", + [], + address1 + ); + expect(getResult).toStrictEqual(Cl.uint(500)); + }); + + it("should fail set-protocol-fee-bps if > 10%", () => { + const { result } = simnet.callPublicFn( + contractName, + "set-protocol-fee-bps", + [Cl.uint(1500)], // 15% - too high + deployer + ); + expect(result).toBeErr(Cl.uint(4)); // ERR-INVALID-AMOUNT + }); + + it("should set admin-point-price successfully", () => { + const { result } = simnet.callPublicFn( + contractName, + "set-admin-point-price", + [Cl.uint(2000)], // 2000 micro-STX per point + deployer + ); + expect(result).toBeOk(Cl.bool(true)); + + const { result: getResult } = simnet.callReadOnlyFn( + contractName, + "get-admin-point-price", + [], + address1 + ); + expect(getResult).toStrictEqual(Cl.uint(2000)); + }); + + it("should fail set-admin-point-price if 0", () => { + const { result } = simnet.callPublicFn( + contractName, + "set-admin-point-price", + [Cl.uint(0)], + deployer + ); + expect(result).toBeErr(Cl.uint(4)); // ERR-INVALID-AMOUNT + }); + + it("should set starting-points successfully", () => { + const { result } = simnet.callPublicFn( + contractName, + "set-starting-points", + [Cl.uint(500)], + deployer + ); + expect(result).toBeOk(Cl.bool(true)); + + const { result: getResult } = simnet.callReadOnlyFn( + contractName, + "get-starting-points", + [], + address1 + ); + expect(getResult).toStrictEqual(Cl.uint(500)); + + // New user should get 500 points + simnet.callPublicFn(contractName, "register", [Cl.stringAscii("bob")], address2); + const { result: pointsResult } = simnet.callReadOnlyFn( + contractName, + "get-user-points", + [Cl.principal(address2)], + address2 + ); + expect(pointsResult).toBeOk(Cl.some(Cl.uint(500))); + }); + + it("should fail all setters if not admin", () => { + expect(simnet.callPublicFn(contractName, "set-min-earned-for-sell", [Cl.uint(5000)], address1).result).toBeErr(Cl.uint(2)); + expect(simnet.callPublicFn(contractName, "set-listing-fee", [Cl.uint(5000000)], address1).result).toBeErr(Cl.uint(2)); + expect(simnet.callPublicFn(contractName, "set-protocol-fee-bps", [Cl.uint(500)], address1).result).toBeErr(Cl.uint(2)); + expect(simnet.callPublicFn(contractName, "set-admin-point-price", [Cl.uint(2000)], address1).result).toBeErr(Cl.uint(2)); + expect(simnet.callPublicFn(contractName, "set-starting-points", [Cl.uint(500)], address1).result).toBeErr(Cl.uint(2)); + }); + }); + describe("get-transaction-log", () => { beforeEach(() => { simnet.callPublicFn(contractName, "register", [Cl.stringAscii("alice")], address1);