Source Code

;; title: sBTC-BitDeriv
;; version:
;; summary:
;; description:

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-EXPIRED (err u1))
(define-constant ERR-INVALID-AMOUNT (err u2))
(define-constant ERR-UNAUTHORIZED (err u3))
(define-constant ERR-NOT-FOUND (err u4))
(define-constant ERR-INSUFFICIENT-BALANCE (err u5))
(define-constant ERR-ALREADY-EXECUTED (err u6))
(define-constant ERR-INVALID-PRICE (err u7))
(define-constant ERR-OPTION-NOT-ACTIVE (err u8))
(define-constant ERR-INVALID-SIGNATURE (err u9))

;; Option Types
(define-constant CALL u1)
(define-constant PUT u2)

;; Data Maps
(define-map BTCBalances
  { holder: principal }
  { balance: uint }
)

;; Escrow map to track BTC locked in options
(define-map EscrowedBTC
  { option-id: uint }
  { amount: uint }
)

(define-map Options
  { option-id: uint }
  {
    creator: principal,
    buyer: (optional principal),
    option-type: uint,
    strike-price: uint,
    premium: uint,
    expiry: uint,
    btc-amount: uint,
    is-active: bool,
    is-executed: bool,
    creation-time: uint,
  }
)

(define-map UserOptions
  { user: principal }
  {
    created: (list 20 uint),
    purchased: (list 20 uint),
  }
)

;; Data Variables
(define-data-var next-option-id uint u0)
(define-data-var oracle-price uint u0)
(define-data-var oracle-public-key (buff 33) 0x000000000000000000000000000000000000000000000000000000000000000000)

;; Authorization
(define-private (is-contract-owner)
  (is-eq tx-sender CONTRACT-OWNER)
)

;; BTC Balance Management
(define-public (deposit-btc (amount uint))
  (let (
      (sender tx-sender)
      (current-balance (default-to { balance: u0 } (map-get? BTCBalances { holder: sender })))
    )
    (map-set BTCBalances { holder: sender } { balance: (+ amount (get balance current-balance)) })
    (ok true)
  )
)

(define-public (withdraw-btc (amount uint))
  (let (
      (sender tx-sender)
      (current-balance (default-to { balance: u0 } (map-get? BTCBalances { holder: sender })))
    )
    (asserts! (>= (get balance current-balance) amount) ERR-INSUFFICIENT-BALANCE)
    (map-set BTCBalances { holder: sender } { balance: (- (get balance current-balance) amount) })
    (ok true)
  )
)

(define-private (transfer-btc
    (from principal)
    (to principal)
    (amount uint)
  )
  (let (
      (from-balance (default-to { balance: u0 } (map-get? BTCBalances { holder: from })))
      (to-balance (default-to { balance: u0 } (map-get? BTCBalances { holder: to })))
    )
    (asserts! (>= (get balance from-balance) amount) ERR-INSUFFICIENT-BALANCE)
    (map-set BTCBalances { holder: from } { balance: (- (get balance from-balance) amount) })
    (map-set BTCBalances { holder: to } { balance: (+ amount (get balance to-balance)) })
    (ok true)
  )
)

(define-private (update-user-options
    (user principal)
    (option-id uint)
    (is-creator bool)
  )
  (let ((user-options (default-to {
      created: (list),
      purchased: (list),
    }
      (map-get? UserOptions { user: user })
    )))
    (if is-creator
      (ok (map-set UserOptions { user: user } {
        created: (unwrap! (as-max-len? (append (get created user-options) option-id) u20)
          ERR-UNAUTHORIZED
        ),
        purchased: (get purchased user-options),
      }))
      (ok (map-set UserOptions { user: user } {
        created: (get created user-options),
        purchased: (unwrap!
          (as-max-len? (append (get purchased user-options) option-id) u20)
          ERR-UNAUTHORIZED
        ),
      }))
    )
  )
)

;; Option Management
(define-public (create-option
    (option-type uint)
    (strike-price uint)
    (premium uint)
    (expiry uint)
    (btc-amount uint)
  )
  (let (
      (option-id (+ (var-get next-option-id) u1))
      (sender tx-sender)
      (current-balance (default-to { balance: u0 } (map-get? BTCBalances { holder: sender })))
    )
    ;; Validate inputs
    (asserts! (or (is-eq option-type CALL) (is-eq option-type PUT))
      ERR-INVALID-AMOUNT
    )
    (asserts! (> strike-price u0) ERR-INVALID-PRICE)
    (asserts! (> premium u0) ERR-INVALID-PRICE)
    (asserts! (> expiry stacks-block-time) ERR-EXPIRED)
    (asserts! (> btc-amount u0) ERR-INVALID-AMOUNT)

    ;; For CALL options, ensure creator has enough BTC
    (asserts!
      (if (is-eq option-type CALL)
        (>= (get balance current-balance) btc-amount)
        true
      )
      ERR-INSUFFICIENT-BALANCE
    )

    ;; Create option
    (map-set Options { option-id: option-id } {
      creator: sender,
      buyer: none,
      option-type: option-type,
      strike-price: strike-price,
      premium: premium,
      expiry: expiry,
      btc-amount: btc-amount,
      is-active: true,
      is-executed: false,
      creation-time: stacks-block-time,
    })

    ;; Lock BTC for CALL options by reducing creator's balance and tracking in escrow
    (if (is-eq option-type CALL)
      (begin
        ;; Deduct BTC from creator's balance
        (map-set BTCBalances { holder: sender } { balance: (- (get balance current-balance) btc-amount) })
        ;; Add to escrow
        (map-set EscrowedBTC { option-id: option-id } { amount: btc-amount })
      )
      true
    )

    ;; Update state
    (var-set next-option-id option-id)
    (try! (update-user-options sender option-id true))
    (ok option-id)
  )
)

;; Private helper functions
(define-private (exercise-call
    (option-id uint)
    (option {
      creator: principal,
      buyer: (optional principal),
      option-type: uint,
      strike-price: uint,
      premium: uint,
      expiry: uint,
      btc-amount: uint,
      is-active: bool,
      is-executed: bool,
      creation-time: uint,
    })
    (holder principal)
  )
  (let (
      (strike-amount (* (get strike-price option) (get btc-amount option)))
      (holder-balance (default-to { balance: u0 } (map-get? BTCBalances { holder: holder })))
    )
    ;; Transfer strike price from holder to creator
    (try! (stx-transfer? strike-amount holder (get creator option)))
    ;; Transfer escrowed BTC to holder
    (map-set BTCBalances { holder: holder } { balance: (+ (get balance holder-balance) (get btc-amount option)) })
    ;; Remove from escrow
    (map-delete EscrowedBTC { option-id: option-id })
    (ok true)
  )
)

(define-public (exercise-option (option-id uint))
  (let (
      (option (unwrap! (map-get? Options { option-id: option-id }) ERR-NOT-FOUND))
      (holder tx-sender)
    )
    ;; Validate option state
    (asserts! (is-eq (some holder) (get buyer option)) ERR-UNAUTHORIZED)
    (asserts! (not (get is-executed option)) ERR-ALREADY-EXECUTED)
    (asserts! (< stacks-block-time (get expiry option)) ERR-EXPIRED)

    ;; Exercise based on option type
    (if (is-eq (get option-type option) CALL)
      (try! (exercise-call option-id option holder))
      (try! (exercise-put option-id option holder))
    )

    ;; Update option state
    (map-set Options { option-id: option-id }
      (merge option { is-executed: true })
    )
    (ok true)
  )
)

(define-private (exercise-put
    (option-id uint)
    (option {
      creator: principal,
      buyer: (optional principal),
      option-type: uint,
      strike-price: uint,
      premium: uint,
      expiry: uint,
      btc-amount: uint,
      is-active: bool,
      is-executed: bool,
      creation-time: uint,
    })
    (holder principal)
  )
  (let (
      (strike-amount (* (get strike-price option) (get btc-amount option)))
      (creator-balance (default-to { balance: u0 }
        (map-get? BTCBalances { holder: (get creator option) })
      ))
    )
    ;; Transfer BTC from holder to creator
    (try! (transfer-btc holder (get creator option) (get btc-amount option)))
    ;; Transfer strike price from creator to holder
    (try! (stx-transfer? strike-amount (get creator option) holder))
    (ok true)
  )
)

;; Read-only functions
(define-read-only (get-option (option-id uint))
  (map-get? Options { option-id: option-id })
)

(define-read-only (get-btc-balance (holder principal))
  (default-to { balance: u0 } (map-get? BTCBalances { holder: holder }))
)

(define-read-only (get-user-options (user principal))
  (default-to {
    created: (list),
    purchased: (list),
  }
    (map-get? UserOptions { user: user })
  )
)

;; Metadata functions using to-ascii? (Clarity 4 feature)

;; ACTUAL to-ascii? USAGE: Convert utf8 string to ascii
(define-read-only (convert-utf8-to-ascii (text (string-utf8 100)))
  (to-ascii? text)
)

;; Example: Get option label with to-ascii? conversion
(define-read-only (get-option-label (option-id uint))
  (match (map-get? Options { option-id: option-id })
    option (let ((label-utf8 (if (is-eq (get option-type option) CALL)
        u"BTC-CALL-Option"
        u"BTC-PUT-Option"
      )))
      (to-ascii? label-utf8)
    )
    (err u999)
  )
)

;; Get human-readable option type name
(define-read-only (get-option-type-name (option-type uint))
  (if (is-eq option-type CALL)
    (some "CALL")
    (if (is-eq option-type PUT)
      (some "PUT")
      none
    )
  )
)

;; Get option status description
(define-read-only (get-option-status (option-id uint))
  (match (map-get? Options { option-id: option-id })
    option (if (get is-executed option)
      (some "EXECUTED")
      (if (>= stacks-block-time (get expiry option))
        (some "EXPIRED")
        (if (is-some (get buyer option))
          (some "ACTIVE-PURCHASED")
          (some "ACTIVE-AVAILABLE")
        )
      )
    )
    none
  )
)

;; Get full option description with all details
(define-read-only (get-option-details (option-id uint))
  (match (map-get? Options { option-id: option-id })
    option (some {
      type: (unwrap-panic (get-option-type-name (get option-type option))),
      status: (unwrap-panic (get-option-status option-id)),
      strike-price: (get strike-price option),
      premium: (get premium option),
      btc-amount: (get btc-amount option),
      expiry: (get expiry option),
      creator: (get creator option),
      buyer: (get buyer option),
    })
    none
  )
)

;; Oracle functions
(define-public (set-btc-price (price uint))
  (begin
    (asserts! (is-contract-owner) ERR-UNAUTHORIZED)
    (var-set oracle-price price)
    (ok true)
  )
)

;; Set oracle public key (only contract owner)
(define-public (set-oracle-public-key (pubkey (buff 33)))
  (begin
    (asserts! (is-contract-owner) ERR-UNAUTHORIZED)
    (var-set oracle-public-key pubkey)
    (ok true)
  )
)

;; Verified price update using secp256r1-verify (Clarity 4 feature)
;; This ensures the price comes from a trusted oracle with cryptographic proof
(define-public (set-btc-price-verified
    (price uint)
    (timestamp uint)
    (signature (buff 64))
  )
  (let (
      (message-hash (sha256 (concat (concat (uint-to-buff price) (uint-to-buff timestamp)) 0x42544300))) ;; "BTC" suffix
      (pubkey (var-get oracle-public-key))
    )
    ;; Verify the signature using secp256r1
    (asserts! (secp256r1-verify message-hash signature pubkey)
      ERR-INVALID-SIGNATURE
    )

    ;; Optional: Add timestamp validation to prevent replay attacks
    (asserts! (>= timestamp stacks-block-time) ERR-EXPIRED)

    ;; Update price
    (var-set oracle-price price)
    (ok true)
  )
)

;; Helper to convert uint to buffer for hashing
(define-private (uint-to-buff (value uint))
  (unwrap-panic (to-consensus-buff? value))
)

(define-read-only (get-btc-price)
  (var-get oracle-price)
)

(define-read-only (get-oracle-public-key)
  (var-get oracle-public-key)
)

Functions (18)

FunctionAccessArgs
is-contract-ownerprivate
deposit-btcpublicamount: uint
withdraw-btcpublicamount: uint
exercise-optionpublicoption-id: uint
get-optionread-onlyoption-id: uint
get-btc-balanceread-onlyholder: principal
get-user-optionsread-onlyuser: principal
convert-utf8-to-asciiread-onlytext: (string-utf8 100
get-option-labelread-onlyoption-id: uint
get-option-type-nameread-onlyoption-type: uint
get-option-statusread-onlyoption-id: uint
get-option-detailsread-onlyoption-id: uint
set-btc-pricepublicprice: uint
set-oracle-public-keypublicpubkey: (buff 33
set-btc-price-verifiedpublicprice: uint, timestamp: uint, signature: (buff 64
uint-to-buffprivatevalue: uint
get-btc-priceread-only
get-oracle-public-keyread-only