Source Code

;; runes-capsule.clar
;; Trustless bridge for Runes deposits TO SIP-10 minting ~ Square Runes tokens
;; Verifies BTC deposits to multisig capsules and mints wrapped tokens

;; ============================================
;; TRAITS
;; ============================================
;; create a square runes trait with burn and mint functions
(use-trait sr-trait .square-runes-trait.square-runes-trait) ;; 'SP3XXMS38VTAWTVPE5682XSBFXPTH7XCPEBTX8AN2.faktory-trait-v1.sip-010-trait) 
;; ============================================
;; ERROR CODES
;; ============================================
(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-CAPSULE-NOT-FOUND (err u402))
(define-constant ERR-DEPOSIT-ALREADY-PROCESSED (err u403))
(define-constant ERR-RUNE-NOT-SUPPORTED (err u404))
(define-constant ERR-MULTISIG-NOT-FOUND (err u405))
(define-constant ERR-INVALID-AMOUNT (err u406))
(define-constant ERR-MINT-FAILED (err u407))
(define-constant ERR-PARSE-FAILED (err u408))
(define-constant ERR-WRONG-OUTPUT (err u409))
(define-constant ERR-TX-NOT-MINED (err u410))
(define-constant ERR-MULTISIG-VERIFICATION-FAILED (err u411))
(define-constant ERR-MULTISIG-MISMATCH (err u412)) 
(define-constant ERR-PUBKEY-MISMATCH (err u413))

;; ============================================
;; CONSTANTS
;; ============================================
;; set of multiple operators decentralized for liveness and redundancy
(define-constant OPERATOR_PUBKEY 0x024289ad7d50c0be883566d38249d0d644ed0626ce52ea0e1e4a9bdafe6293bc21)

;; Multisig threshold (2-of-2)
(define-constant MULTISIG_THRESHOLD u2)

;; Expected output index for capsule deposits (output 1 = multisig)
(define-constant EXPECTED_MULTISIG_OUTPUT u1)

;; Contract deployer
(define-constant CONTRACT_DEPLOYER tx-sender)

;; ============================================
;; DATA VARS
;; ============================================
(define-data-var contract-owner principal tx-sender)
(define-data-var mom-token-contract principal .square-mom) ;; Set after MOM token deployment

(define-data-var processed-tx-count uint u1)

;; ============================================
;; DATA MAPS
;; ============================================

;; Maps supported Runes to their SIP-10 token contracts
;; Key: { block: uint, tx: uint } TO Value: token contract principal
(define-map supported-runes 
  { block: uint, tx: uint }
  { token-contract: principal, decimals: uint, name: (string-ascii 32) }
)

;; Maps capsule scriptPubKey to owner's Stacks address
;; Key: scriptPubKey (buff 34) TO Value: { owner: principal, registered-at: uint }
(define-map capsule-owners
  (buff 34)
  { owner: principal, registered-at: uint }
)

;; Tracks processed deposits to prevent double-minting
;; Key: btc-tx-id (buff 32) TO Value: deposit details
(define-map processed-btc-txs
  (buff 128)
  { 
    amount: uint,
    capsule-script-pubkey: (buff 34),
    stx-receiver: principal,
    edict-tag: (optional uint),
    edict-output: uint,
    pointer-output: (optional uint),
    rune-block: uint,
    rune-tx: uint,
    tx-number: uint,
    tag: uint,
    btc-height: uint
  }
)

;; ============================================
;; INITIALIZATION
;; ============================================
(begin
  ;; Register MOM's as first supported Rune
  (map-set supported-runes 
    { block: u922359, tx: u1350 }
    { 
      token-contract: .square-mom, 
      decimals: u0,
      name: "MOM"
    }
  )
)

;; ============================================
;; ADMIN FUNCTIONS
;; ============================================

;; no admins, use code-body instead
(define-public (set-contract-owner (new-owner principal))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (ok (var-set contract-owner new-owner))
  )
)

;; use code-body instead?
(define-public (add-supported-rune 
    (rune-block uint) 
    (rune-tx uint) 
    (token-contract principal)
    (decimals uint)
    (name (string-ascii 32))
  )
  (begin
    (asserts! (is-eq tx-sender (var-get contract-owner)) ERR-NOT-AUTHORIZED)
    (ok (map-set supported-runes 
      { block: rune-block, tx: rune-tx }
      { token-contract: token-contract, decimals: decimals, name: name }
    ))
  )
)

;; ============================================
;; CAPSULE REGISTRATION
;; ============================================

;; User registers their capsule by proving they control the multisig
;; user-pubkey: the user's compressed pubkey (derived from their Stacks address)
;; capsule-script: the P2WSH scriptPubKey (0x0020 + 32-byte witness program)
(define-public (register-capsule 
    (stx-receiver principal)
    (user-pubkey (buff 33)) 
    (capsule-script (buff 34))
    (is-segwit bool)
  )
  (begin 
      (asserts! (is-eq (principal-of? user-pubkey) (ok stx-receiver)) ERR-PUBKEY-MISMATCH)
        (let (
            ;; Build the pubkey list: [user-pubkey, operator-pubkey]
            (pubkeys (list OPERATOR_PUBKEY user-pubkey))  ;; OPERATOR first
            
            ;; Verify the multisig address matches
            (verification (unwrap! (contract-call? 
                .multisig-verify 
                verify-multisig-address
                pubkeys
                MULTISIG_THRESHOLD
                is-segwit
                (some capsule-script)
            ) ERR-MULTISIG-VERIFICATION-FAILED)))

            (asserts! verification ERR-MULTISIG-MISMATCH)
            ;; Map the capsule scriptPubKey to tx-sender
            (ok (map-set capsule-owners 
            capsule-script
            { owner: stx-receiver, registered-at: burn-block-height } ;; pubkey is not persisted
            ))
  )
))

;; ============================================
;; READ-ONLY HELPERS
;; ============================================

(define-read-only (get-capsule-owner (capsule-script (buff 34)))
  (map-get? capsule-owners capsule-script)
)

(define-read-only (get-supported-rune (rune-block uint) (rune-tx uint))
  (map-get? supported-runes { block: rune-block, tx: rune-tx })
)

(define-read-only (is-deposit-processed (btc-tx-id (buff 128)))
  (is-some (map-get? processed-btc-txs btc-tx-id))
)

(define-read-only (get-deposit-info (btc-tx-id (buff 128)))
  (map-get? processed-btc-txs btc-tx-id)
)

;; ============================================
;; CORE DEPOSIT PROCESSING
;; ============================================

;; Main entry point: Process a Runes deposit with full merkle proof verification
(define-public (process-deposit
    (height uint)
    (wtx {
      version: (buff 4),
      ins: (list 50 { outpoint: { hash: (buff 32), index: (buff 4) }, scriptSig: (buff 1376), sequence: (buff 4) }),
      outs: (list 50 { value: (buff 8), scriptPubKey: (buff 1376) }),
      locktime: (buff 4),
    })
    (witness-data (buff 1650))
    (header (buff 80))
    (tx-index uint)
    (tree-depth uint)
    (wproof (list 14 (buff 32)))
    (witness-merkle-root (buff 32))
    (witness-reserved-value (buff 32))
    (ctx (buff 4096))
    (cproof (list 14 (buff 32)))
    (mom-token <sr-trait>)
  )
  (let (
      ;; Build full tx buffer
      (tx-buff (contract-call?
        'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.bitcoin-helper-wtx-v2
        concat-wtx wtx witness-data
      ))
    )
    ;; Verify tx was mined on Bitcoin
    (match (contract-call?
      'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.clarity-bitcoin-lib-v7
      was-segwit-tx-mined-compact height tx-buff header tx-index tree-depth
      wproof witness-merkle-root witness-reserved-value ctx cproof
    )
      btc-tx-id (process-verified-deposit btc-tx-id tx-buff height mom-token)
      error (err (* error u1000)) ;; ERR-TX-NOT-MINED
    )
  )
)

;; Process deposit after BTC tx is verified as mined
(define-private (process-verified-deposit 
    (btc-tx-id (buff 128))
    (tx-buff (buff 4096))
    (btc-height uint)
    (sq-rune <sr-trait>)
  )
  (begin
    ;; Check not already processed
    (asserts! (is-none (map-get? processed-btc-txs btc-tx-id)) ERR-DEPOSIT-ALREADY-PROCESSED)
    
    ;; Parse the OP_RETURN to get Rune info
    (let (
        (parsed (unwrap! (parse-deposit-opreturn tx-buff) ERR-PARSE-FAILED))
        (amount (get amount parsed))
        (edict-output (unwrap! (get edict-output parsed) ERR-PARSE-FAILED))
        (edict-tag (get edict-tag parsed))
        (pointer-output (get pointer-output parsed))
        (rune-block (get rune-block parsed))
        (rune-tx (get rune-tx parsed))
        (tag (get tag parsed))
      )
      
      ;; Verify this is MOM's or another supported Rune
      (let (
          (rune-info (unwrap! (get-supported-rune rune-block rune-tx) ERR-RUNE-NOT-SUPPORTED))
        )
        
        ;; Verify the output points to the capsule multisig
        (let (
            (multisig-output (unwrap! (get-output-at-index tx-buff edict-output) ERR-MULTISIG-NOT-FOUND))
            (output-script (get scriptPubKey multisig-output))
          )
          
          ;; Get the capsule owner
          (let (
              (capsule-owner-info (unwrap! 
                (get-capsule-owner (unwrap! (as-max-len? output-script u34) ERR-CAPSULE-NOT-FOUND))
                ERR-CAPSULE-NOT-FOUND
              ))
              (owner (get owner capsule-owner-info))
              (current-count (var-get processed-tx-count))
            )
            
            ;; Record the deposit
            (map-set processed-btc-txs btc-tx-id {
              amount: amount,
              capsule-script-pubkey: (unwrap! (as-max-len? output-script u34) ERR-CAPSULE-NOT-FOUND),
              stx-receiver: owner,
              edict-output: edict-output,
              edict-tag: edict-tag,
              pointer-output: pointer-output,
              rune-block: rune-block,
              rune-tx: rune-tx,
              tag: tag,
              btc-height: btc-height,
              tx-number: current-count
            })

            (var-set processed-tx-count (+ current-count u1))

            ;; Mint tokens to the owner
            (try! (contract-call? sq-rune mint amount owner))
            
            ;; Emit event
            (print {
              type: "runes-deposit-processed",
              btc-tx-id: btc-tx-id,
              amount: amount,
              capsule-script-pubkey: (unwrap! (as-max-len? output-script u34) ERR-CAPSULE-NOT-FOUND),
              stx-receiver: owner,
              edict-output: edict-output,
              edict-tag: edict-tag,
              pointer-output: pointer-output,
              rune-block: rune-block,
              rune-tx: rune-tx,
              tag: tag,
              btc-height: btc-height,
              tx-number: current-count
            })
            
            (ok {
              btc-tx-id: btc-tx-id,
              owner: owner,
              amount: amount,
              rune: (get name rune-info)
            })
          )
        )
      )
    )
  )
)

;; ============================================
;; PARSING HELPERS (using runes-decoder)
;; ============================================

;; Parse OP_RETURN from tx buffer using the runes-decoder library
(define-read-only (parse-deposit-opreturn (tx-buff (buff 4096)))
  (let (
      (output0 (unwrap! (get-output-at-index tx-buff u0) (err u500)))
      (script (get scriptPubKey output0))
    )
    ;; decode-any-runestone is more generic - handles Tag 0, 11, 22
    (contract-call? .runes-decoder decode-any-runestone script)
  )
)

;; Get output at specific index from parsed tx
;; is this different for legacy Rafa?
(define-read-only (get-output-at-index (tx (buff 4096)) (index uint))
  (let (
      (parsed-tx (contract-call?
        'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.clarity-bitcoin-lib-v7
        parse-wtx tx false
      ))
    )
    (match parsed-tx
      result (let (
          (outs (get outs result))
          (out (unwrap! (element-at? outs index) (err u502)))
        )
        (ok {
          scriptPubKey: (get scriptPubKey out),
          value: (get value out)
        })
      )
      error (err u503) ;; missing
    )
  )
)

;; ============================================
;; TEST FUNCTION (no merkle proof required)
;; ============================================

;; For testing: process deposit without BTC mining verification
(define-public (test-process-deposit
    (wtx {
      version: (buff 4),
      ins: (list 50 { outpoint: { hash: (buff 32), index: (buff 4) }, scriptSig: (buff 1376), sequence: (buff 4) }),
      outs: (list 50 { value: (buff 8), scriptPubKey: (buff 1376) }),
      locktime: (buff 4),
    })
    (witness-data (buff 1650))
    (mock-btc-tx-id (buff 128))  
    (sq-rune <sr-trait>))    
  (let (
      (tx-buff (contract-call?
        'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.bitcoin-helper-wtx-v2
        concat-wtx wtx witness-data
      ))
    )
    (process-verified-deposit mock-btc-tx-id tx-buff u0 sq-rune)
  )
)

;; Simple test: just parse and log without minting
(define-read-only (test-parse-deposit (tx-buff (buff 4096)))
  (let (
      (parsed (parse-deposit-opreturn tx-buff))
      (output0 (get-output-at-index tx-buff u0))
      (output1 (get-output-at-index tx-buff u1))
    )
    {
      parsed-opreturn: parsed,
      output-0: output0,
      output-1: output1
    }
  )
)

Functions (11)

FunctionAccessArgs
set-contract-ownerpublicnew-owner: principal
add-supported-runepublicrune-block: uint, rune-tx: uint, token-contract: principal, decimals: uint, name: (string-ascii 32
register-capsulepublicstx-receiver: principal, user-pubkey: (buff 33
get-capsule-ownerread-onlycapsule-script: (buff 34
get-supported-runeread-onlyrune-block: uint, rune-tx: uint
is-deposit-processedread-onlybtc-tx-id: (buff 128
get-deposit-inforead-onlybtc-tx-id: (buff 128
process-verified-depositprivatebtc-tx-id: (buff 128
parse-deposit-opreturnread-onlytx-buff: (buff 4096
get-output-at-indexread-onlytx: (buff 4096
test-parse-depositread-onlytx-buff: (buff 4096