Source Code

;; boom-payment-hub.clar
;;
;; A payment hub contract that:
;; 1. Receives STX or SIP-010 tokens with payment metadata
;; 2. Enforces platform fees on-chain by payment type
;; 3. Splits payment between recipient and Boom treasury
;; 4. Emits standardized payment events for chainhook detection

;; SIP-010 trait for fungible tokens
(use-trait sip010-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)

;; ============================================
;; Constants
;; ============================================

;; Contract owner for admin functions
(define-constant CONTRACT_OWNER tx-sender)

;; Boom treasury for fee collection
;; TODO: Update to actual treasury principal before mainnet deployment
(define-constant BOOM_TREASURY 'SP3GWP6DSGA7XX8FW7388YZ9DWT8WJDY2VZXWPV1C)

;; Max payment amount to prevent overflow in fee calculation
;; (uint max / 10000 to ensure fee-bps multiplication is safe)
(define-constant MAX_PAYMENT_AMOUNT u34028236692093846346337460743176821)

;; Error codes
(define-constant ERR_INVALID_AMOUNT (err u100))
(define-constant ERR_TRANSFER_FAILED (err u101))
(define-constant ERR_TOKEN_NOT_APPROVED (err u103))
(define-constant ERR_NOT_AUTHORIZED (err u104))
(define-constant ERR_UNKNOWN_PAYMENT_TYPE (err u105))
(define-constant ERR_INVALID_FEE_RATE (err u106))
(define-constant ERR_INVALID_PAYMENT_TYPE (err u107))
(define-constant ERR_AMOUNT_TOO_LARGE (err u108))

;; ============================================
;; Data Maps
;; ============================================

;; Approved tokens whitelist (sBTC, USDCx, etc.)
(define-map ApprovedTokens principal bool)

;; Fee rates by payment type (in basis points, 100 = 1%)
;; Enforced on-chain - callers cannot override
(define-map FeeRates 
  { paymentType: (string-ascii 20) }
  { feeBps: uint }
)

;; ============================================
;; Initialize Data
;; ============================================

;; Approved tokens
(map-set ApprovedTokens 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token true)
(map-set ApprovedTokens 'SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx true)

;; Fee rates by payment type
;; name: 0% (100% to Boom treasury as recipient)
;; nft_purchase: 2.5% to Boom, rest to seller
;; nft_mint: 2.5% to Boom, rest to creator
;; tip: 1% to Boom, rest to recipient
;; goods: 3% to Boom, rest to merchant
;; digital: 2.5% to Boom, rest to creator
(map-set FeeRates { paymentType: "name" } { feeBps: u0 })
(map-set FeeRates { paymentType: "nft_purchase" } { feeBps: u250 })
(map-set FeeRates { paymentType: "nft_mint" } { feeBps: u250 })
(map-set FeeRates { paymentType: "tip" } { feeBps: u100 })
(map-set FeeRates { paymentType: "goods" } { feeBps: u300 })
(map-set FeeRates { paymentType: "digital" } { feeBps: u250 })

;; ============================================
;; Read-Only Functions
;; ============================================

;; Get contract info
(define-read-only (get-info)
  (ok {
    name: "Boom Payment Hub",
    version: u3,
    treasury: BOOM_TREASURY,
    maxPaymentAmount: MAX_PAYMENT_AMOUNT
  })
)

;; Check if a token is approved
(define-read-only (is-token-approved (tokenContract principal))
  (default-to false (map-get? ApprovedTokens tokenContract))
)

;; Get fee rate for a payment type (returns 0 if not found)
(define-read-only (get-fee-rate (paymentType (string-ascii 20)))
  (get feeBps (default-to { feeBps: u0 } (map-get? FeeRates { paymentType: paymentType })))
)

;; Check if payment type exists
(define-read-only (is-valid-payment-type (paymentType (string-ascii 20)))
  (is-some (map-get? FeeRates { paymentType: paymentType }))
)

;; Calculate fee and net amounts for a given payment
(define-read-only (calculate-split (amount uint) (paymentType (string-ascii 20)))
  (let (
    (feeBps (get-fee-rate paymentType))
    (fee (/ (* amount feeBps) u10000))
    (net (- amount fee))
  )
    { fee: fee, net: net, feeBps: feeBps }
  )
)

;; ============================================
;; Admin Functions
;; ============================================

;; Add or update fee rate for a payment type
;; Uses tx-sender (not contract-caller) to ensure only the original
;; signer can modify, preventing force contract-call attacks
(define-public (set-fee-rate (paymentType (string-ascii 20)) (feeBps uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (asserts! (<= feeBps u10000) ERR_INVALID_FEE_RATE) ;; Max 100%
    (asserts! (> (len paymentType) u0) ERR_INVALID_PAYMENT_TYPE)
    (ok (map-set FeeRates { paymentType: paymentType } { feeBps: feeBps }))
  )
)

;; Add approved token
(define-public (add-approved-token (tokenContract principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (ok (map-set ApprovedTokens tokenContract true))
  )
)

;; Remove approved token
(define-public (remove-approved-token (tokenContract principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (ok (map-delete ApprovedTokens tokenContract))
  )
)

;; ============================================
;; Payment Functions
;; ============================================

;; Pay with STX
;; Fee is determined on-chain by paymentType - cannot be manipulated by caller
;; 
;; @param recipient - The merchant/seller address to receive net payment
;; @param amount - Total amount in microSTX (fee will be deducted)
;; @param paymentType - Type of payment (determines fee rate)
;; @param reference - Domain-specific ID (name, listing ID, etc.)
(define-public (pay-stx 
    (recipient principal)
    (amount uint)
    (paymentType (string-ascii 20))
    (reference (string-ascii 64))
)
  (let (
    (feeBps (get-fee-rate paymentType))
    (fee (/ (* amount feeBps) u10000))
    (net (- amount fee))
  )
    ;; Validate
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (<= amount MAX_PAYMENT_AMOUNT) ERR_AMOUNT_TOO_LARGE)
    (asserts! (is-valid-payment-type paymentType) ERR_UNKNOWN_PAYMENT_TYPE)

    ;; Transfer fee to Boom treasury (if any)
    (if (> fee u0)
      (try! (stx-transfer? fee tx-sender BOOM_TREASURY))
      true
    )

    ;; Transfer net to recipient
    (try! (stx-transfer? net tx-sender recipient))

    ;; Emit payment event for chainhook
    (print {
      event: "payment",
      version: u3,
      token: "STX",
      paymentType: paymentType,
      reference: reference,
      sender: tx-sender,
      recipient: recipient,
      treasury: BOOM_TREASURY,
      amount: amount,
      feeBps: feeBps,
      fee: fee,
      net: net
    })

    (ok { amount: amount, fee: fee, net: net })
  )
)

;; Pay with SIP-010 token (sBTC, USDCx)
;; Fee is determined on-chain by paymentType - cannot be manipulated by caller
;;
;; @param token - The SIP-010 token contract (must be whitelisted)
;; @param recipient - The merchant/seller address to receive net payment
;; @param amount - Total amount in token base units (fee will be deducted)
;; @param paymentType - Type of payment (determines fee rate)
;; @param reference - Domain-specific ID
(define-public (pay-sip010
    (token <sip010-trait>)
    (recipient principal)
    (amount uint)
    (paymentType (string-ascii 20))
    (reference (string-ascii 64))
)
  (let (
    (tokenContract (contract-of token))
    (feeBps (get-fee-rate paymentType))
    (fee (/ (* amount feeBps) u10000))
    (net (- amount fee))
  )
    ;; Validate token is whitelisted
    (asserts! (is-token-approved tokenContract) ERR_TOKEN_NOT_APPROVED)
    
    ;; Validate amount and payment type
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (<= amount MAX_PAYMENT_AMOUNT) ERR_AMOUNT_TOO_LARGE)
    (asserts! (is-valid-payment-type paymentType) ERR_UNKNOWN_PAYMENT_TYPE)

    ;; Transfer fee to Boom treasury (if any)
    (if (> fee u0)
      (try! (contract-call? token transfer fee tx-sender BOOM_TREASURY none))
      true
    )

    ;; Transfer net to recipient
    (try! (contract-call? token transfer net tx-sender recipient none))

    ;; Emit payment event for chainhook
    (print {
      event: "payment",
      version: u3,
      token: tokenContract,
      paymentType: paymentType,
      reference: reference,
      sender: tx-sender,
      recipient: recipient,
      treasury: BOOM_TREASURY,
      amount: amount,
      feeBps: feeBps,
      fee: fee,
      net: net
    })

    (ok { amount: amount, fee: fee, net: net })
  )
)

Functions (10)

FunctionAccessArgs
get-inforead-only
is-token-approvedread-onlytokenContract: principal
get-fee-rateread-onlypaymentType: (string-ascii 20
is-valid-payment-typeread-onlypaymentType: (string-ascii 20
calculate-splitread-onlyamount: uint, paymentType: (string-ascii 20
set-fee-ratepublicpaymentType: (string-ascii 20
add-approved-tokenpublictokenContract: principal
remove-approved-tokenpublictokenContract: principal
pay-stxpublicrecipient: principal, amount: uint, paymentType: (string-ascii 20
pay-sip010publictoken: <sip010-trait>, recipient: principal, amount: uint, paymentType: (string-ascii 20