Smart Contract

Check it on Mainnet

https://explorer.stacks.co/txid/SP1SCEXE6PMGPAC6B4N5P2MDKX8V4GF9QDE1FNNGJ.trustless-rewards?chain=mainnet

Explanations

Traits used

These traits enforce those standard are implemented in this smart contract

(use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)
(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)

Constants

Used for signaling different errors , an ok messages and the default price used if one can't be found for the lobby

(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-NOT-FOUND (err u404))
(define-constant ERR-NOT-ACTIVE (err u403))
(define-constant ERR-ALREADY-JOINED (err u405))
(define-constant ERR-JOIN-FAILED (err u500))
(define-constant OK-SUCCESS u200)
(define-constant DEFAULT-PRICE u100)

Variable Data Stored - Maps and Vars

Store the information for each lobby.

Store the highscore of each user to that lobby.

Keep the count of created lobbies, also used as id for the next lobby.

Keep the admin that makes the smart contract calls to submit highscores, pay users, end lobbies etc.

(define-map lobbies {id: uint} {owner: principal, description: (string-ascii 99), balance: uint, price: uint, factor: uint, commission: uint, mapy: (string-ascii 30), length: (string-ascii 10), traffic: (string-ascii 10), curves: (string-ascii 10), hours: uint, active: bool})
(define-map scoreboard {lobby-id: uint, address: principal} {score: uint, rank: uint, sum-rank-factor: uint, rank-factor: uint, rewards: uint, rac: uint, nft: (string-ascii 99)})
(define-data-var lobby-count uint u0)
(define-data-var contract-owner principal tx-sender)

General Functions

Functions that anyone can call:

Create a lobby.

Join a lobby.

Get a lobby info - read only. Can get the information without paying STX directly calling the endpoint, as it does not write information to the blockchain.

Get the high score of a user address for a specified lobby-id.

(define-public (create-lobby 
  (description (string-ascii 99)) (price uint) (factor uint) (commission uint) 
  (mapy (string-ascii 30)) (length (string-ascii 10)) (traffic (string-ascii 10)) (curves (string-ascii 10)) (hours uint)
  )
  (let ((lobby-id (increment-lobby-count)))
  (map-set lobbies {id: lobby-id} { owner: tx-sender, description: description, balance: u0, price: price, factor: factor, commission: commission, 
        mapy: mapy, length: length, traffic: traffic, curves: curves, hours: hours, active: true
  })
  (try! (join lobby-id))
  (ok lobby-id)))

(define-public (join (id uint))
  (let ((entry-price (default-to DEFAULT-PRICE (get price (map-get? lobbies {id: id}))))
    (joined (map-insert scoreboard {lobby-id: id, address: tx-sender} {score: u0, rank: u0, sum-rank-factor: u0, rank-factor: u0, rewards: u0, rac: u0, nft: ""})))
    (unwrap-panic (map-get? lobbies {id: id}))
    (asserts! (default-to false (get active (map-get? lobbies {id: id}))) ERR-NOT-ACTIVE)
    (asserts! joined ERR-ALREADY-JOINED)
    (add-balance id tx-sender entry-price)
    (print {action: "join", lobby-id: id, address: tx-sender })
    (ok OK-SUCCESS)))

(define-read-only (get-lobby (id uint))
    (ok (unwrap-panic (map-get? lobbies {id: id}))))

(define-read-only (get-score (lobby-id uint) (address principal))
    (ok (unwrap-panic (map-get? scoreboard {lobby-id: lobby-id, address: address}))))

Admin Functions

Functions are public but only the admin can pass the asserts to make the calls. This is done for security reasons. Without it, anyone would be able to abuse the information. It also facilitates transparency on-chain by letting everybody see what the admin called.

Simple functions

Disable lobby - ends a specific lobby. After ending it high scores cannot be uploaded to it and payments cannot be done to the users from it.

Set owner - change the admin of the smart contract. The old address will not be able to call the admin functions after the call is confirmed, while the new address will be able afterwards.

(define-public (disable-lobby (id uint))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (match
      (map-get? lobbies {id: id})
      lobby
      (map-set lobbies {id: id} (merge lobby {active: false}))
      false)
    (ok true)))
    
(define-public (set-owner (new-owner principal))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (var-set contract-owner new-owner)
    (ok true)))

Complex functions

Publish - posts the high scores to the blockchain. It gets from the admin a list of maximum 50 high scores for one or more active lobbies and sets them in the scoreboard map.

(define-public (publish-result-many (run-result (list 50 { lobby-id: uint, address: principal, score: uint, rank: uint, sum-rank-factor: uint, rank-factor: uint, rewards: uint, rac: uint, nft: (string-ascii 99)})))
  (fold check-err
    (map publish-result run-result)
    (ok true)))

(define-private (publish-result (run-result { lobby-id: uint, address: principal, score: uint, rank: uint, sum-rank-factor: uint, rank-factor: uint, rewards: uint, rac: uint, nft: (string-ascii 99)}))
  (publish-only (get lobby-id run-result) (get address run-result) (get score run-result) (get rank run-result) (get sum-rank-factor run-result) (get rank-factor run-result) (get rewards run-result) (get rac run-result) (get nft run-result)))

(define-private (publish-only (lobby-id uint) (address principal) (score uint) (rank uint) (sum-rank-factor uint) (rank-factor uint) (rewards uint) (rac uint) (nft (string-ascii 99)))
  (let
    ((publishOk (try! (publish lobby-id address score rank sum-rank-factor rank-factor rewards rac nft))))
    (ok publishOk)))

(define-private (publish (lobby-id uint) (address principal) (score uint) (rank uint) (sum-rank-factor uint) (rank-factor uint) (rewards uint) (rac uint) (nft (string-ascii 99)))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (unwrap-panic (map-get? scoreboard {lobby-id: lobby-id, address: address}))
    (asserts! (default-to false (get active (map-get? lobbies {id: lobby-id}))) ERR-NOT-ACTIVE)
    (map-set scoreboard {lobby-id: lobby-id, address: address} {score: score, rank: rank, sum-rank-factor: sum-rank-factor, rank-factor: rank-factor, rewards: rewards, rac: rac, nft: nft})
    (print {action: "publish", lobby-id: lobby-id, address: address, score: score, rank: rank, sum-rank-factor: sum-rank-factor, rank-factor: rank-factor, rewards: rewards, rac: rac, nft: nft})
    (ok true)))

Finish - when a lobby is done, finish it by submitting the last high scores. Calculate off-chain the rewards for each player who took part in that lobby and call it on-chain to distribute them directly from the pool.

(define-public (finish-result-many  (run-result (list 50 { lobby-id: uint, address: principal, score: uint, rank: uint, sum-rank-factor: uint, rank-factor: uint, rewards: uint, rac: uint, nft: (string-ascii 99)})))
  (fold check-err
    (map finish-result run-result)
    (ok true)))
    
(define-private (finish-result (run-result { lobby-id: uint, address: principal, score: uint, rank: uint, sum-rank-factor: uint, rank-factor: uint, rewards: uint, rac: uint, nft: (string-ascii 99)}))
    (finish-only (get lobby-id run-result) (get address run-result) (get score run-result) (get rank run-result) (get sum-rank-factor run-result) (get rank-factor run-result) (get rewards run-result) (get rac run-result) (get nft run-result)))
    
(define-private (finish-only (lobby-id uint) (address principal) (score uint) (rank uint) (sum-rank-factor uint) (rank-factor uint) (rewards uint) (rac uint) (nft (string-ascii 99)))
  (let
  ((finishOk (try! (finish lobby-id address score rank sum-rank-factor rank-factor rewards rac nft))))
  (ok finishOk)))
  
;; distribute rewards for all runs in a lobby
(define-private (finish (lobby-id uint) (address principal) (score uint) (rank uint) (sum-rank-factor uint) (rank-factor uint) (rewards uint) (rac uint) (nft (string-ascii 99)))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (unwrap-panic (map-get? scoreboard {lobby-id: lobby-id, address: address}))
    (asserts! (default-to false (get active (map-get? lobbies {id: lobby-id}))) ERR-NOT-ACTIVE)
    (map-set scoreboard {lobby-id: lobby-id, address: address} {score: score, rank: rank, sum-rank-factor: sum-rank-factor, rank-factor: rank-factor, rewards: rewards, rac: rac, nft: nft})
    (try! (as-contract (stx-transfer? rac tx-sender address)))
    (print {action: "finish", lobby-id: lobby-id, address: address, score: score, rank: rank, sum-rank-factor: sum-rank-factor, rank-factor: rank-factor, rewards: rewards, rac: rac, nft: nft})
    (ok true)))

Helper Functions

Functions are private and directly used when they are needed, in other functions.

Increment lobby id.

Add balance - send x amount of STX from the tx-sender and add it to the lobby pool.

Check response is error.is

(define-private (increment-lobby-count)
  (begin
    (var-set lobby-count (+ (var-get lobby-count) u1))
    (var-get lobby-count)))

(define-private (add-balance (id uint) (participant principal) (amount uint))
  (begin
    (unwrap-panic (stx-transfer? amount participant (as-contract tx-sender)))
    (match
      (map-get? lobbies {id: id})
      lobby
      (map-set lobbies {id: id} (merge lobby {balance: (+ (default-to u0 (get balance (map-get? lobbies {id: id}))) amount)}))
      false)))

(define-private (check-err (result (response bool uint)) (prior (response bool uint)))
  (match prior ok-value result
    err-value (err err-value)))

Safety Functions

Implement the standard trait for transferring assets: STX, FT and NFTs.

(define-public (transfer-stx (address principal) (amount uint))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (unwrap-panic (as-contract (stx-transfer? amount (as-contract tx-sender) address)))
    (ok true)))

(define-public (transfer-ft-token (address principal) (amount uint) (token <ft-trait>))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (try! (as-contract (contract-call? token transfer amount tx-sender address none)))
    (ok true)))

(define-public (transfer-nft-token (address principal) (id uint) (token <nft-trait>))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (try! (as-contract (contract-call? token transfer id tx-sender address)))
    (ok true)))

Last updated