nft-marketplace
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.
| Severity | Count |
|---|---|
| CRITICAL | 6 |
| HIGH | 5 |
| MEDIUM | 8 |
| LOW / INFO | 5 |
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:
- Fractional:
fractionalize-nftcreates a vault but the owner retains the NFT. They can sell fractions and then transfer the NFT away — fraction holders own shares of nothing. - Rental:
rent-nftcharges rent and collateral but the NFT remains with the owner. The "rental" is purely bookkeeping with no on-chain access control. - Staking:
stake-nftrecords a stake but the NFT remains freely transferable. Users can earn staking rewards while selling their NFT elsewhere.
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
- No events: None of the contracts emit
printstatements for off-chain indexing. - No upgrade path: All contracts use
define-constant contract-ownerwith no ownership transfer. - Stats can desync: Multiple sequential
map-setcalls for statistics tracking — if intermediate operations fail, counts become inconsistent. - Staking tiers defined but unused: The
stake-tiersmap is populated butget-stake-bonususes hardcoded if/else instead of reading the map. - No re-listing mechanism: After cancellation, old listing IDs remain as inactive entries. No way to relist without creating a new ID.
Architecture
| Contract | Role | Lines | Functions | Maps |
|---|---|---|---|---|
| nft-marketplace | Fixed-price sales & auctions | ~230 | 10 | 4 |
| nft-collection | SIP-009 NFT with minting | ~195 | 15 | 5 |
| nft-fractional | Fractionalize NFTs into shares | ~210 | 9 | 4 |
| nft-rental | Time-limited NFT rental | ~250 | 9 | 4 |
| nft-staking | Stake NFTs for STX rewards | ~210 | 11 | 4 |
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.