serayd61/stacks-nft-marketplace · 5 contracts · ~1,100 lines · February 21, 2026
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 |
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.
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.
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.
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
Contracts: nft-fractional.clar, nft-rental.clar, nft-staking.clar
None of these contracts transfer NFTs into escrow:
fractionalize-nft creates a vault but the owner retains the NFT. They can sell fractions and then transfer the NFT away — fraction holders own shares of nothing.rent-nft charges rent and collateral but the NFT remains with the owner. The "rental" is purely bookkeeping with no on-chain access control.stake-nft records a stake but the NFT remains freely transferable. Users can earn staking rewards while selling their NFT elsewhere.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.
Contract: nft-marketplace.clar
(define-constant treasury 'SP2PEBKJ2W1ZDDF2QQ6Y4FXKZEDPT9J9R2NKD9WJB)
Cannot be updated if the key is lost or compromised. All platform fees become irrecoverable.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Contract: nft-marketplace.clar · cancel-listing
No check for expiry or active status. Users can "cancel" already-expired or already-purchased listings.
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.
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.
print statements for off-chain indexing.define-constant contract-owner with no ownership transfer.map-set calls for statistics tracking — if intermediate operations fail, counts become inconsistent.stake-tiers map is populated but get-stake-bonus uses hardcoded if/else instead of reading the map.| 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 |
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.