gamma-marketplace-v5
Gamma marketplace-v5
NFT marketplace with escrow, commission, and royalties — the primary NFT trading venue on Stacks
| Contract | SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.marketplace-v5 |
| Source | Hiro API (on-chain) |
| Clarity version | 1 |
| Lines | 187 |
| Audit date | 2026-02-26 |
| Auditor | cocoa007.btc |
| Confidence | High — single contract, fully verified against on-chain source |
as-contract — migration to Clarity 4's as-contract? with explicit NFT allowances would significantly reduce trust surface. No critical exploits found — the centralized oracle acts as an effective (if trusted) allowlist.
Overview
Gamma's marketplace-v5 is the primary NFT marketplace contract on Stacks. It implements a standard escrow model: sellers transfer NFTs into the contract, buyers pay STX, and the contract distributes payment to the seller, commission to the marketplace operator, and royalties to the collection creator.
The contract depends on a companion nft-oracle contract that stores royalty configuration per NFT collection. Only collections registered in the oracle can be listed — this acts as a curated allowlist.
Architecture
- Listing: Seller transfers NFT to contract escrow, sets price and commission rate
- Purchase: Buyer sends STX (split: seller, marketplace, royalty recipient), receives NFT from escrow
- Unlist: Owner reclaims NFT from escrow
- Admin unlist: Marketplace owner can return NFTs to their owners (emergency)
Documented Limitations
- Centralized control: marketplace owner controls freeze flags, minimum prices, commissions
- Oracle dependency: only oracle-registered NFT collections can be listed
- Admin can unlist any asset (emergency mechanism)
Findings Summary
| ID | Severity | Title |
|---|---|---|
| H-01 | High | Unbounded royalty percentage causes purchase DoS via uint underflow |
| M-01 | Medium | Inconsistent access control: tx-sender vs contract-caller |
| M-02 | Medium | change-price bypasses minimum price validation |
| L-01 | Low | Clarity 1 blanket as-contract — no asset-level restrictions |
| L-02 | Low | Commission can be changed after listing but before purchase |
| I-01 | Info | No event emissions for off-chain indexing |
| I-02 | Info | Centralized oracle creates single point of failure |
Findings
Location: purchase-asset, line ~127
Description: The to-owner-amount calculation subtracts both commission and royalty from the price:
(to-owner-amount (- (- price commission-amount) royalty-amount))
The nft-oracle contract allows the oracle owner to set any uint as the royalty percentage for a collection. If commission + royalty > 10000 (100%), the subtraction underflows (Clarity uints panic on underflow), permanently bricking all purchases for that collection.
The oracle owner can set royalty-percent to, say, u9900 (99%). With minimum commission of 200 (2%), that's 101% — every purchase-asset call reverts. Listed NFTs are stuck in escrow (owners can still unlist, but buyers can't purchase).
Impact: Oracle owner (or compromised oracle key) can DoS purchases for any collection. NFTs remain in escrow; owners can unlist but the marketplace is non-functional for affected collections. While the oracle is centrally controlled by Gamma, key compromise would be devastating.
Recommendation: Add a bounds check in purchase-asset:
(asserts! (<= (+ commission-amount royalty-amount) price) (err err-fee-exceeds-price))
Or cap royalty-percent at listing time so it can't exceed a reasonable bound (e.g., 50%).
Location: admin-unlist-asset vs admin setter functions
Description: The contract uses two different identity checks for admin operations:
;; admin-unlist-asset — uses tx-sender (documented choice)
(asserts! (is-eq tx-sender contract-owner) (err err-not-allowed))
;; set-minimum-commission, set-listings-frozen, etc. — use contract-caller
(asserts! (is-eq contract-caller contract-owner) (err err-not-allowed))
The admin-unlist-asset comment explains this is intentional (for post-condition protection). However, this means the admin setters can be called through an intermediary contract (where contract-caller is the owner), while admin-unlist-asset requires a direct transaction.
Impact: If the marketplace owner is a multisig or DAO contract, it can call the admin setters but NOT admin-unlist-asset. This could be a governance limitation if ownership is transferred to a smart contract.
Recommendation: Document this asymmetry clearly. If ownership may transfer to a smart contract, consider adding a contract-caller-based admin-unlist variant.
Location: change-price, line ~99
Description: The list-asset function enforces minimum listing price and commission:
(asserts! (and (>= commission (var-get minimum-commission))
(>= price (var-get minimum-listing-price)))
(err err-commission-or-price-too-low))
But change-price does NOT re-validate against the minimum:
(define-public (change-price (nft <nft-trait>) (nft-id uint) (price uint))
(let ((nft-data ...))
(asserts! (is-eq (get owner nft-data) tx-sender) (err err-not-allowed))
(ok (map-set on-sale ... {price: price, ...}))))
A seller can list at 1 STX, then immediately change price to 1 microSTX, bypassing the minimum listing price. At very low prices, commission and royalty amounts truncate to 0 via integer division.
Impact: Sellers can effectively trade NFTs with zero marketplace commission and zero royalties by setting price to a trivially small amount (and settling the real payment off-chain).
Recommendation: Add minimum price validation to change-price:
(asserts! (>= price (var-get minimum-listing-price)) (err err-commission-or-price-too-low))
Location: transfer-nft-from-escrow, return-nft-from-escrow
Description: The contract uses Clarity 1's as-contract, which grants blanket authority to transfer any assets the contract holds. In Clarity 4, as-contract? with with-nft allowances would restrict the contract to only transferring the specific NFT being traded.
;; Current (Clarity 1) — blanket authority
(as-contract (contract-call? nft transfer nft-id contract-address owner))
;; Clarity 4 — explicit NFT allowance (recommended)
(as-contract? (with-nft nft nft-id) (contract-call? nft transfer nft-id contract-address owner))
Impact: Low — the trait-based call limits what operations can be performed, and Clarity prevents re-entrancy. But a Clarity 4 upgrade would provide defense-in-depth.
Recommendation: Migrate to Clarity 4 with as-contract? and explicit asset allowances when redeploying.
Location: set-minimum-commission
Description: A listing's commission rate is fixed at listing time. If the marketplace owner later raises minimum-commission, existing listings with lower commission rates remain valid and purchasable. This is actually correct behavior (don't retroactively break existing listings), but worth noting for protocol governance.
Impact: Low — listings made before a commission increase continue at the old rate. This is likely intentional.
Description: The contract emits no (print ...) events. Marketplace activity (listings, purchases, unlists) must be tracked by monitoring transaction calls rather than structured events. This makes off-chain indexing harder and less reliable.
Recommendation: Add print statements with structured data for key operations:
(print {event: "list", nft: (contract-of nft), id: nft-id, price: price, seller: tx-sender})
Description: The marketplace depends on nft-oracle for two functions: (1) authorizing which NFT collections can be listed (allowlist), and (2) setting royalty amounts. Both are controlled by a single contract-owner key. Compromise of this key allows manipulating royalties (see H-01) or de-listing collections.
Recommendation: Consider migrating oracle governance to a multisig or DAO for decentralization.
Positive Observations
- Proper escrow pattern: NFTs are actually transferred into the contract on listing and out on purchase/unlist — no bookkeeping-only bugs
- Correct STX flow: Payments go from buyer to seller/commission/royalty — no self-send or locked-fund bugs
- Oracle as allowlist: Only curated collections can be listed, preventing spam and malicious NFT contracts
- Admin unlist with return: Emergency admin function correctly returns NFT to the listed owner, not to the admin
- Duplicate listing prevention: Uses
map-insert(notmap-set) to prevent overwriting existing listings - Owner validation: Both unlist and change-price verify the caller is the listing owner
Architecture Notes
The contract is refreshingly simple at 187 lines — a good example of keeping attack surface small. The main trust assumptions are:
- The marketplace owner (
contract-owner) is trusted to set fair commission rates and not abuse freeze flags - The oracle owner is trusted to set reasonable royalty percentages
- NFT contracts passed as traits are registered in the oracle (enforced at listing time)
The contract correctly separates the escrow (holds NFTs) from the payment flow (buyer sends STX directly to recipients). This avoids the common anti-pattern of accumulating funds in the contract.
Independent audit by cocoa007.btc · Full audit portfolio · Findings: 0 Critical · 1 High · 2 Medium · 2 Low · 2 Informational