Source Code

;; NeuralMint Auction Contract
;; English-style open auction with auto-extend, min bid increments, and refund mechanism
;; Demonstrates block-height based timing (Proof of Transfer awareness)

(use-trait nft-trait .sip009-nft-trait.nft-trait)

;; -- Error Constants --
(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-NOT-FOUND (err u404))
(define-constant ERR-INVALID-AMOUNT (err u400))
(define-constant ERR-AUCTION-ENDED (err u410))
(define-constant ERR-AUCTION-ACTIVE (err u411))
(define-constant ERR-BID-TOO-LOW (err u412))
(define-constant ERR-NO-BIDS (err u413))
(define-constant ERR-PAUSED (err u503))
(define-constant ERR-SELF-BID (err u414))

;; -- Configuration --
(define-constant CONTRACT-OWNER tx-sender)
(define-constant PLATFORM-FEE-BPS u100) ;; 1%
(define-constant MIN-DURATION u6)       ;; ~1 hour minimum (6 blocks)
(define-constant AUTO-EXTEND-BLOCKS u3) ;; ~30 min auto-extend if bid in last 30 min

;; -- State --
(define-data-var last-auction-id uint u0)
(define-data-var platform-wallet principal tx-sender)
(define-data-var contract-paused bool false)
(define-data-var total-auctions-settled uint u0)
(define-data-var total-volume uint u0)

;; -- Auction Data --
(define-map auctions uint {
  seller: principal,
  nft-contract: principal,
  token-id: uint,
  reserve-price: uint,
  min-bid-increment: uint,
  highest-bid: uint,
  highest-bidder: (optional principal),
  end-block: uint,
  settled: bool,
  cancelled: bool
})

;; -- Read-Only Functions --

(define-read-only (get-auction (auction-id uint))
  (ok (map-get? auctions auction-id))
)

(define-read-only (get-auction-status (auction-id uint))
  (let ((auction (unwrap! (map-get? auctions auction-id) ERR-NOT-FOUND)))
    (ok {
      is-active: (and
        (not (get settled auction))
        (not (get cancelled auction))
        (<= block-height (get end-block auction))
      ),
      blocks-remaining: (if (<= block-height (get end-block auction))
        (- (get end-block auction) block-height)
        u0
      ),
      highest-bid: (get highest-bid auction),
      highest-bidder: (get highest-bidder auction),
      bid-count: u0
    })
  )
)

(define-read-only (get-platform-stats)
  (ok {
    total-auctions: (var-get last-auction-id),
    total-settled: (var-get total-auctions-settled),
    total-volume: (var-get total-volume)
  })
)

;; -- Public Functions --

;; Create a new auction
(define-public (create-auction
  (nft-contract <nft-trait>)
  (token-id uint)
  (reserve-price uint)
  (min-bid-increment uint)
  (duration uint)
)
  (let (
    (auction-id (+ (var-get last-auction-id) u1))
    (owner (unwrap! (contract-call? nft-contract get-owner token-id) ERR-NOT-FOUND))
    (end-block (+ block-height duration))
  )
    (asserts! (not (var-get contract-paused)) ERR-PAUSED)
    (asserts! (is-eq owner (some tx-sender)) ERR-NOT-AUTHORIZED)
    (asserts! (> reserve-price u0) ERR-INVALID-AMOUNT)
    (asserts! (> min-bid-increment u0) ERR-INVALID-AMOUNT)
    (asserts! (>= duration MIN-DURATION) ERR-INVALID-AMOUNT)

    ;; Escrow NFT
    (try! (contract-call? nft-contract transfer token-id tx-sender (as-contract tx-sender)))

    (map-set auctions auction-id {
      seller: tx-sender,
      nft-contract: (contract-of nft-contract),
      token-id: token-id,
      reserve-price: reserve-price,
      min-bid-increment: min-bid-increment,
      highest-bid: u0,
      highest-bidder: none,
      end-block: end-block,
      settled: false,
      cancelled: false
    })

    (var-set last-auction-id auction-id)
    (ok auction-id)
  )
)

;; Place a bid - automatically refunds previous highest bidder
(define-public (place-bid (auction-id uint) (bid-amount uint))
  (let (
    (auction (unwrap! (map-get? auctions auction-id) ERR-NOT-FOUND))
    (min-required (if (> (get highest-bid auction) u0)
      (+ (get highest-bid auction) (get min-bid-increment auction))
      (get reserve-price auction)
    ))
  )
    (asserts! (not (var-get contract-paused)) ERR-PAUSED)
    (asserts! (not (get settled auction)) ERR-AUCTION-ENDED)
    (asserts! (not (get cancelled auction)) ERR-AUCTION-ENDED)
    (asserts! (<= block-height (get end-block auction)) ERR-AUCTION-ENDED)
    (asserts! (>= bid-amount min-required) ERR-BID-TOO-LOW)
    (asserts! (not (is-eq tx-sender (get seller auction))) ERR-SELF-BID)

    ;; Escrow new bid
    (try! (stx-transfer? bid-amount tx-sender (as-contract tx-sender)))

    ;; Refund previous highest bidder
    (match (get highest-bidder auction)
      prev-bidder (try! (as-contract (stx-transfer? (get highest-bid auction) tx-sender prev-bidder)))
      true
    )

    ;; Auto-extend if bid is placed near the end
    (let ((new-end-block (if (<= (- (get end-block auction) block-height) AUTO-EXTEND-BLOCKS)
          (+ (get end-block auction) AUTO-EXTEND-BLOCKS)
          (get end-block auction)
        )))
      (map-set auctions auction-id (merge auction {
        highest-bid: bid-amount,
        highest-bidder: (some tx-sender),
        end-block: new-end-block
      }))
    )

    (ok true)
  )
)

;; Settle auction - anyone can call after expiry
(define-public (settle-auction (auction-id uint) (nft-contract <nft-trait>))
  (let (
    (auction (unwrap! (map-get? auctions auction-id) ERR-NOT-FOUND))
    (winner (unwrap! (get highest-bidder auction) ERR-NO-BIDS))
    (final-price (get highest-bid auction))
    (platform-fee (/ (* final-price PLATFORM-FEE-BPS) u10000))
    (seller-amount (- final-price platform-fee))
  )
    (asserts! (not (get settled auction)) ERR-AUCTION-ENDED)
    (asserts! (not (get cancelled auction)) ERR-AUCTION-ENDED)
    (asserts! (> block-height (get end-block auction)) ERR-AUCTION-ACTIVE)

    ;; Pay seller from escrow
    (try! (as-contract (stx-transfer? seller-amount tx-sender (get seller auction))))
    ;; Pay platform fee
    (try! (as-contract (stx-transfer? platform-fee tx-sender (var-get platform-wallet))))

    ;; Transfer NFT to winner from escrow
    (try! (as-contract (contract-call? nft-contract transfer (get token-id auction) tx-sender winner)))

    ;; Update state
    (map-set auctions auction-id (merge auction { settled: true }))
    (var-set total-auctions-settled (+ (var-get total-auctions-settled) u1))
    (var-set total-volume (+ (var-get total-volume) final-price))

    (ok { winner: winner, price: final-price })
  )
)

;; Cancel auction - only seller, only if no bids
(define-public (cancel-auction (auction-id uint) (nft-contract <nft-trait>))
  (let ((auction (unwrap! (map-get? auctions auction-id) ERR-NOT-FOUND)))
    (asserts! (is-eq tx-sender (get seller auction)) ERR-NOT-AUTHORIZED)
    (asserts! (not (get settled auction)) ERR-AUCTION-ENDED)
    (asserts! (not (get cancelled auction)) ERR-AUCTION-ENDED)
    (asserts! (is-none (get highest-bidder auction)) ERR-AUCTION-ACTIVE)

    ;; Return NFT to seller from escrow
    (try! (as-contract (contract-call? nft-contract transfer (get token-id auction) tx-sender (get seller auction))))

    (map-set auctions auction-id (merge auction { cancelled: true }))
    (ok true)
  )
)

;; -- Admin Functions --

(define-public (set-platform-wallet (new-wallet principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (var-set platform-wallet new-wallet)
    (ok true)
  )
)

(define-public (set-paused (paused bool))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (var-set contract-paused paused)
    (ok true)
  )
)

Functions (8)

FunctionAccessArgs
get-auctionread-onlyauction-id: uint
get-auction-statusread-onlyauction-id: uint
get-platform-statsread-only
place-bidpublicauction-id: uint, bid-amount: uint
settle-auctionpublicauction-id: uint, nft-contract: <nft-trait>
cancel-auctionpublicauction-id: uint, nft-contract: <nft-trait>
set-platform-walletpublicnew-wallet: principal
set-pausedpublicpaused: bool