Content

Clarity of Mind Foreword Introduction

Setup

We start our project setup as usual: clarinet new tiny-market. Once inside the project folder, we will create the tiny market contract using clarinet contract new tiny-market. Since our marketplace revolves around selling NFTs, the first thing we have to do is add the SIP009 trait and create a SIP009 NFT. We already made a few of these so we will leave it as a challenge to the reader. Make sure the SIP009 contract is called sip009-nft-trait and the NFT contract sip009-nft. You may implement the contract any way you like as long it has a mint function that the contract deployer can call. We will use it later for our unit tests. Here is an example:

(define-public (mint (recipient principal))
    (let ((token-id (+ (var-get token-id-nonce) u1)))
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (try! (nft-mint? stacksies token-id recipient))
        (asserts! (var-set token-id-nonce token-id) err-token-id-failure)
        (ok token-id)
    )
)

Once you have done that, let us add the SIP010 trait and create a SIP010 token as well. We will call these sip010-ft-trait and sip010-token. The SIP010 token should also have a mint function that only the contract deployer can call.

(define-public (mint (amount uint) (recipient principal))
    (begin
        (asserts! (is-eq tx-sender contract-owner) err-owner-only)
        (ft-mint? amazing-coin amount recipient)
    )
)

Trait imports & constants

Let us now work on tiny-market. We will start off by importing the SIP traits. Be sure to check the naming and change them if you used different names. Do not forget to also edit the depends_on section in Clarinet.toml like you have learned. We also define a constant for the contract owner.

(use-trait nft-trait .sip009-nft-trait.sip009-nft-trait)
(use-trait ft-trait .sip010-ft-trait.sip010-ft-trait)

(define-constant contract-owner tx-sender)

We will then think about the various error states that exist in our marketplace. The act of listing an NFT may fail under a number of circumstances; namely, the expiry block height is in the past, or the listing price is zero (we will not allow free listings). There is also the consideration of the using trying to list the NFT not actually owning it, but this will be handled by the NFT contract itself. You may remember that the built-in NFT functions fail with an error code if the NFT does not exist or if it is not owned by tx-sender. We will simply propagate those errors using control flow functions. We therefore only define two listing error codes:

;; listing errors
(define-constant err-expiry-in-past (err u1000))
(define-constant err-price-zero (err u1001))

When it comes to cancelling and fulfilling, there are a few more error conditions we can identify:

  • The listing the tx-sender wants to cancel or fulfil does not exist.
  • The tx-sender tries to cancel a listing it did not create.
  • The listing the tx-sender tries to fill has expired.
  • The provided NFT asset trait reference does not match the NFT contract of the listing. Since trait references cannot be stored directly in Clarity, they will have to be provided again when the buyer is trying to purchase an NFT. We have to make sure that the trait reference provided by the buyer matches the NFT contract provided by the seller.
  • The provided payment asset trait reference does not match the payment asset contract of the listing. The same as the above but for the SIP010 being used to purchase the NFT.
  • The maker and the taker (seller and the buyer) are equal. We will not permit users to purchase tokens from themselves using the same principal.
  • The buyer is not the intended taker. If the seller defines an intended taker (buyer) for the listing, then only that principal can fulfil the listing.

Finally, we will implement a whitelist for NFT and payment asset contracts that the contract deployer controls. It makes for two additional error conditions:

  • The NFT asset the seller is trying to list is not whitelisted.
  • The requested payment asset is not whitelisted.

Turning all of these into unique error constants, we get something like the following:

;; cancelling and fulfilling errors
(define-constant err-unknown-listing (err u2000))
(define-constant err-unauthorised (err u2001))
(define-constant err-listing-expired (err u2002))
(define-constant err-nft-asset-mismatch (err u2003))
(define-constant err-payment-asset-mismatch (err u2004))
(define-constant err-maker-taker-equal (err u2005))
(define-constant err-unintended-taker (err u2006))
(define-constant err-asset-contract-not-whitelisted (err u2007))
(define-constant err-payment-contract-not-whitelisted (err u2008))

Data storage

The marketplace itself only has to store a little information regarding the listing. The most efficient way to store the individual listings is by using a data map that uses an unsigned integer as a key. The integer functions as a unique identifier and will increment for each new listing. We will never reuse a value. To track the latest listing ID, we will use a simple data variable.

(define-map listings
    uint
    {
        maker: principal,
        taker: (optional principal),
        token-id: uint,
        nft-asset-contract: principal,
        expiry: uint,
        price: uint,
        payment-asset-contract: (optional principal)
    }
)

(define-data-var listing-nonce uint u0)

It is important to utilise the native types in Clarity to the fullest extent possible. A listing does not need to have an intended taker, so we make it optional. The same goes for the payment asset. If the seller wants to be paid in STX, then there is no payment asset. If the seller wants to be paid using a SIP010 token, then its token contract will be stored.

Asset whitelist

We will implement an asset whitelist to keep our marketplace safe. Only the contract owner will have the ability to modify the whitelist. The whitelist itself is a simple map that stores a boolean for a given contract principal. A guarded public function set-whitelisted is used to update the whitelist and a read-only function is-whitelisted allows anyone to check if a particular contract is whitelisted or not. We will also use is-whitelisted to guard other public functions later.

(define-map whitelisted-asset-contracts principal bool)

(define-read-only (is-whitelisted (asset-contract principal))
    (default-to false (map-get? whitelisted-asset-contracts asset-contract))
)

(define-public (set-whitelisted (asset-contract principal) (whitelisted bool))
    (begin
        (asserts! (is-eq contract-owner tx-sender) err-unauthorised)
        (ok (map-set whitelisted-asset-contracts asset-contract whitelisted))
    )
)