Coding the Marketplace Contract
In this step, we build the core logic of our marketplace contract in Clarity, which allows us to list, cancel, and purchase 3D NFTs using both STX and FT (fungible tokens), potentially, sats from sBTC.
3D NFTs also includes support for royalties and whitelisting allowed asset contracts.
Traits Imported
(use-trait nft-trait '...nft-trait.nft-trait)
(use-trait ft-trait '...sip-010-trait-ft-standard.sip-010-trait)
We import the standard NFT and FT traits to interact with SIP-009 and SIP-010-compliant tokens.
Constants
(define-constant CONTRACT_OWNER 'ST...)
(define-constant PAGE_SIZE u10)
CONTRACT_OWNER
: the address that can manage the whitelist.PAGE_SIZE
: maximum number of listings returned per page.
Error Codes
(define-constant ERR_EXPIRY_IN_PAST (err u1000))
...
These codes represent different failure states: invalid expiration, zero pricing, identity conflicts, or unauthorized contracts.
Data Structures and Variables
(define-data-var listing-nonce uint u0)
(define-map listings uint { ... })
(define-map whitelisted-asset-contracts principal bool)
We use listing-nonce
as an incremental counter, and listings
to store each active listing.
Events
(define-event list-event ...)
(define-event sale-event ...)
(define-event cancel-event ...)
These events are triggered when assets are listed, sold, or canceled.
Read-Only Functions
(define-read-only (is-whitelisted ...))
(define-read-only (get-listing ...))
(define-read-only (get-listings ...))
These functions allow us to fetch listing metadata from the frontend.
Admin Functions
(define-public (set-whitelisted ...))
Enables the CONTRACT_OWNER
to approve which NFT or FT contracts are allowed in the marketplace.
Internal Helpers
(define-private (safe-transfer-nft ...))
(define-private (safe-transfer-ft ...))
(define-private (pay-royalty ...))
Utility functions for secure asset transfers and transparent royalty payments.
Listing NFTs
(define-public (list-asset ...))
Verifies conditions, transfers the NFT to the contract, and stores the listing in the map. Includes list-assets-batch
to list multiple assets at once.
Cancel Listing
(define-public (cancel-listing ...))
Reverts the listing and returns the NFT to the original owner.
Internal Validations
(define-private (assert-can-fulfil ...))
Ensures the buyer is valid, the listing is still active, and the contracts match.
Purchase with STX or FT
(define-public (fulfil-listing-stx ...))
(define-public (fulfil-listing-ft ...))
Transfers the NFT to the buyer, sends the payment to the seller (optionally with royalties), and deletes the listing.
Check the full code:
;; 4V4 Marketplace Contract
;; Traits
(use-trait nft-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)
(use-trait ft-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
;; Constants
(define-constant CONTRACT_OWNER 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA)
(define-constant PAGE_SIZE u10)
;; Error Codes
(define-constant ERR_EXPIRY_IN_PAST (err u1000))
(define-constant ERR_PRICE_ZERO (err u1001))
(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 Vars
(define-data-var listing-nonce uint u0)
(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-map whitelisted-asset-contracts principal bool)
;; Events
(define-event list-event (listing-id uint) (maker principal))
(define-event sale-event (listing-id uint) (buyer principal))
(define-event cancel-event (listing-id uint))
;; Read-only
(define-read-only (is-whitelisted (asset-contract principal))
(default-to true (map-get? whitelisted-asset-contracts asset-contract))
)
(define-read-only (get-listing (listing-id uint))
(map-get? listings listing-id)
)
(define-read-only (get-listing-price (listing-id uint))
(ok (get price (unwrap-panic (map-get? listings listing-id))))
)
(define-read-only (get-listings (start uint))
(map
(lambda (i)
(map-get? listings (+ start i))
)
(range u0 PAGE_SIZE)
)
)
;; Admin
(define-public (set-whitelisted (asset-contract principal) (whitelisted bool))
(begin
(asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORISED)
(map-set whitelisted-asset-contracts asset-contract whitelisted)
(ok true)
)
)
;; NFT Transfer Helper
(define-private (safe-transfer-nft (contract principal) (token-id uint) (from principal) (to principal))
(match (contract-call? contract transfer token-id from to)
result (ok result)
(err u5001))
)
;; FT Transfer Helper
(define-private (safe-transfer-ft (contract principal) (amount uint) (from principal) (to principal))
(match (contract-call? contract transfer amount from to none)
result (ok result)
(err u5002))
)
;; Internal Royalty Call (Optional)
(define-private (pay-royalty (contract <nft-trait>) (token-id uint) (amount uint) (payer principal))
(match (contract-call? contract get-royalty-info token-id)
royalty
(begin
(let ((recipient (get recipient royalty)) (bps (get bps royalty)))
(let ((fee (/ (* amount bps) u10000)))
(try! (stx-transfer? fee payer recipient))
)
)
)
(ok true)
)
)
;; Listing Creation
(define-public (list-asset
(nft-asset-contract principal)
(nft-asset {
taker: (optional principal),
token-id: uint,
expiry: uint,
price: uint,
payment-asset-contract: (optional principal)
})
)
(let ((listing-id (var-get listing-nonce)))
(asserts! (is-whitelisted nft-asset-contract) ERR_ASSET_CONTRACT_NOT_WHITELISTED)
(asserts! (> (get expiry nft-asset) block-height) ERR_EXPIRY_IN_PAST)
(asserts! (> (get price nft-asset) u0) ERR_PRICE_ZERO)
(asserts! (match (get payment-asset-contract nft-asset)
payment-asset (is-whitelisted payment-asset)
true) ERR_PAYMENT_CONTRACT_NOT_WHITELISTED)
(try! (safe-transfer-nft nft-asset-contract (get token-id nft-asset) tx-sender (as-contract tx-sender)))
(map-set listings listing-id (merge
{ maker: tx-sender, nft-asset-contract: nft-asset-contract }
nft-asset
))
(var-set listing-nonce (+ listing-id u1))
(print (list-event listing-id tx-sender))
(ok listing-id)
)
)
(define-public (list-assets-batch (nft-asset-contract <nft-trait>) (assets (list 50 (tuple
taker: (optional principal),
token-id: uint,
expiry: uint,
price: uint,
payment-asset-contract: (optional principal)
))))
(map
(lambda (asset)
(list-asset nft-asset-contract asset)
)
assets
)
)
;; Cancel Listing
(define-public (cancel-listing (listing-id uint) (nft-asset-contract principal))
(let ((listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)))
(asserts! (is-eq (get maker listing) tx-sender) ERR_UNAUTHORISED)
(asserts! (is-eq (get nft-asset-contract listing) nft-asset-contract) ERR_NFT_ASSET_MISMATCH)
(map-delete listings listing-id)
(try! (safe-transfer-nft nft-asset-contract (get token-id listing) (as-contract tx-sender) tx-sender))
(print (cancel-event listing-id))
(ok true)
)
)
;; Internal Validation
(define-private (assert-can-fulfil
(nft-asset-contract principal)
(payment-asset-contract (optional principal))
(listing (tuple
maker: principal,
taker: (optional principal),
token-id: uint,
nft-asset-contract: principal,
expiry: uint,
price: uint,
payment-asset-contract: (optional principal)
))
)
(begin
(asserts! (not (is-eq (get maker listing) tx-sender)) ERR_MAKER_TAKER_EQUAL)
(asserts! (match (get taker listing) intended (is-eq intended tx-sender) true) ERR_UNINTENDED_TAKER)
(asserts! (< block-height (get expiry listing)) ERR_LISTING_EXPIRED)
(asserts! (is-eq (get nft-asset-contract listing) nft-asset-contract) ERR_NFT_ASSET_MISMATCH)
(asserts! (is-eq (get payment-asset-contract listing) payment-asset-contract) ERR_PAYMENT_ASSET_MISMATCH)
(ok true)
)
)
;; Fulfil (STX)
(define-public (fulfil-listing-stx (listing-id uint) (nft-asset-contract principal))
(let ((listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)))
(try! (assert-can-fulfil nft-asset-contract none listing))
(try! (pay-royalty nft-asset-contract (get token-id listing) (get price listing) tx-sender))
(try! (safe-transfer-nft nft-asset-contract (get token-id listing) (as-contract tx-sender) tx-sender))
(try! (stx-transfer? (get price listing) tx-sender (get maker listing)))
(map-delete listings listing-id)
(print (sale-event listing-id tx-sender))
(ok listing-id)
)
)
;; Fulfil (FT)
(define-public (fulfil-listing-ft (listing-id uint) (nft-asset-contract principal) (payment-asset-contract principal))
(let ((listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)))
(try! (assert-can-fulfil nft-asset-contract (some payment-asset-contract) listing))
(try! (safe-transfer-nft nft-asset-contract (get token-id listing) (as-contract tx-sender) tx-sender))
(try! (safe-transfer-ft payment-asset-contract (get price listing) tx-sender (get maker listing)))
(map-delete listings listing-id)
(print (sale-event listing-id tx-sender))
(ok listing-id)
)
)}
Conclusion
This contract forms the core transaction engine of the 4V4 marketplace. It supports safe listings, purchases via STX or tokens, contract validation, and extensibility for royalties and batch minting.
In the next step, we'll test the contracts properly.