stacks-nft-marketplace — NFT Ecosystem

serayd61/stacks-nft-marketplace · 5 contracts · ~1,100 lines · February 21, 2026

Overview

A comprehensive NFT ecosystem comprising five contracts: a fixed-price marketplace with auctions (nft-marketplace.clar), an SIP-009 NFT collection (nft-collection.clar), fractional NFT ownership (nft-fractional.clar), NFT rental (nft-rental.clar), and NFT staking for rewards (nft-staking.clar).

The fundamental flaw across all five contracts: none of them actually custody NFTs. The marketplace collects payment but never delivers NFTs. The fractional system sells shares of NFTs it doesn't hold. The rental system charges fees for access it can't enforce. The staking system rewards users for locking tokens that remain freely transferable. Additionally, multiple as-contract bugs cause funds to be permanently locked.

SeverityCount
CRITICAL6
HIGH5
MEDIUM8
LOW / INFO5

Critical Findings

C-1 No NFT Escrow — Sellers Never Transfer NFTs

Contract: nft-marketplace.clar · list-nft, buy-nft

The marketplace never takes custody of NFTs. list-nft creates a map entry but never calls nft-transfer?. buy-nft sends STX to the seller but never delivers the NFT to the buyer. A seller can list, collect payment, and the buyer receives nothing.

;; list-nft: only creates a map entry
(map-set listings listing-id { seller: tx-sender, ... })
;; No nft-transfer? to escrow

;; buy-nft: pays seller, never delivers NFT
(try! (stx-transfer? seller-amount tx-sender seller))
;; No nft-transfer? to buyer

The same issue applies to create-auction and end-auction — NFTs are never escrowed or delivered.

C-2 Auction Bids Not Escrowed — Free Bidding

Contract: nft-marketplace.clar · place-bid

Bids are recorded but bidder funds are never transferred. Only a balance check is performed:

(asserts! (>= (stx-get-balance tx-sender) bid-amount) err-insufficient-funds)
;; No stx-transfer? — funds stay with bidder

When end-auction settles, it attempts stx-transfer? from the winner who may no longer have sufficient funds. Previous bidders' funds are never refunded because they were never collected.

C-3 claim-buyout-proceeds Sends STX to Itself

Contract: nft-fractional.clar · claim-buyout-proceeds

The as-contract block makes tx-sender refer to the contract itself, so funds transfer from contract → contract (a no-op):

;; Inside as-contract, tx-sender = contract principal
(try! (as-contract (stx-transfer? share-of-buyout tx-sender tx-sender)))
;; Contract sends to itself — fraction holders get nothing

All buyout proceeds are permanently locked. The outer tx-sender (the claimer) must be captured in a let binding before entering as-contract.

C-4 claim-collateral Sends Collateral to Contract, Not Owner

Contract: nft-rental.clar · claim-collateral

Identical as-contract bug — collateral from delinquent renters is transferred from the contract back to itself:

(try! (as-contract (stx-transfer? (get collateral-paid rental) tx-sender tx-sender)))
;; NFT owner never receives the collateral

C-5 No NFT Custody in Fractional, Rental, or Staking Contracts

Contracts: nft-fractional.clar, nft-rental.clar, nft-staking.clar

None of these contracts transfer NFTs into escrow:

C-6 Anyone Can List or Auction Any NFT

Contract: nft-marketplace.clar · list-nft, create-auction

Neither function verifies that tx-sender owns the specified NFT. Combined with C-1, an attacker lists someone else's NFT, a buyer pays, and the attacker collects the funds.

High Severity Findings

H-1 Hardcoded Treasury Address

Contract: nft-marketplace.clar

(define-constant treasury 'SP2PEBKJ2W1ZDDF2QQ6Y4FXKZEDPT9J9R2NKD9WJB)

Cannot be updated if the key is lost or compromised. All platform fees become irrecoverable.

H-2 No Duplicate Listing Prevention

Contract: nft-marketplace.clar

The same NFT can be listed multiple times simultaneously. A seller creates multiple listings for the same token and collects payment from multiple buyers. No per-NFT active listing tracking exists.

H-3 Batch Mint Hardcoded to 10, Overcharges for count > 10

Contract: nft-collection.clar · mint-batch

;; Charges for `count` tokens...
(try! (stx-transfer? (* mint-price count) tx-sender contract-owner))
;; ...but only iterates 10 times
(fold mint-single (list u1 u2 u3 u4 u5 u6 u7 u8 u9 u10) ...)

If count = 20, user pays for 20 tokens but only receives 10. The excess payment is stolen.

H-4 Buyout Price Manipulation by Vault Owner

Contract: nft-fractional.clar · update-fraction-price

The vault owner can change the fraction price at any time, which recalculates the buyout price as total-fractions × new-price × 2. After selling fractions cheaply, the owner raises the price to make buyout prohibitively expensive, trapping minority shareholders.

H-5 Rental Platform Fee Goes to Immutable contract-owner

Contract: nft-rental.clar

Platform fees are sent to contract-owner (a constant set at deploy time). Unlike a configurable treasury, this address can never be changed.

Medium Severity Findings

M-1 Integer Truncation in Fee Calculations

Contracts: nft-marketplace.clar, nft-rental.clar

For small prices, (/ (* price 250) 10000) truncates to zero. Platform collects no fee on trades under 40 STX (marketplace) or small rental amounts.

M-2 Approval System Has No Effect

Contract: nft-collection.clar

The transfer function only checks (is-eq tx-sender sender) but never checks token-approvals or operator-approvals. The entire ERC-721-style approval system is decorative — approved operators cannot transfer tokens.

M-3 Token URIs Are All Identical

Contract: nft-collection.clar · mint

(map-set token-uris token-id (concat (var-get base-uri) ""))

Concatenates base URI with empty string. All tokens get the same metadata URI. Token ID is never appended.

M-4 Royalties Recorded But Never Enforced

Contract: nft-collection.clar, nft-marketplace.clar

royalty-info and calculate-royalty exist in the collection contract, but the marketplace's buy-nft never calls them. Creators receive no royalty payments on secondary sales.

M-5 emergency-withdraw Can Underflow reward-pool

Contract: nft-staking.clar

(var-set reward-pool (- (var-get reward-pool) amount))

No bounds check. If amount > reward-pool, the subtraction causes an underflow panic, bricking the function.

M-6 Expired Listings Still Cancellable

Contract: nft-marketplace.clar · cancel-listing

No check for expiry or active status. Users can "cancel" already-expired or already-purchased listings.

M-7 Staking Rewards Without Actual Lock

Contract: nft-staking.clar

Since NFTs are never escrowed (C-5), stakers earn rewards based on lock duration while the NFT remains freely transferable. A user could "stake" an NFT, sell it on a marketplace, and continue earning staking rewards.

M-8 Anyone Can Fund Reward Pool

Contract: nft-staking.clar · add-to-reward-pool

No access control on pool funding. Combined with emergency-withdraw (admin-only), this enables a bait-and-switch: temporarily fund the pool to attract stakers, then drain it.

Informational

Architecture

ContractRoleLinesFunctionsMaps
nft-marketplaceFixed-price sales & auctions~230104
nft-collectionSIP-009 NFT with minting~195155
nft-fractionalFractionalize NFTs into shares~21094
nft-rentalTime-limited NFT rental~25094
nft-stakingStake NFTs for STX rewards~210114

Verdict

This ecosystem has a fatal architectural flaw: none of the five contracts ever take custody of NFTs. Every operation — buying, selling, auctioning, fractionalizing, renting, staking — is purely bookkeeping with no on-chain enforcement. Combined with as-contract bugs that permanently lock funds (C-3, C-4), and the ability to list NFTs you don't own (C-6), the entire suite is not safe for production use. A complete redesign with proper NFT escrow via contract-call? is required.