Source Code

;; PasskeyVault - Clarity Smart Contract
;; ============================================
;; A secure personal vault with cryptographic signature authentication
;; Showcases advanced Clarity features:
;; 1. secp256k1-verify - Signature verification
;; 2. contract-code-hash - Validate external contracts (Clarity 4)
;; 3. Post-conditions via explicit checks - Protect vault assets
;; 4. stx-account - Query account balances
;; 5. Timestamps via burn-block-height patterns
;; 6. Principal validation and access control

;; ============================================
;; CONSTANTS
;; ============================================

(define-constant CONTRACT_OWNER tx-sender)

;; Error Codes
(define-constant ERR_NOT_AUTHORIZED (err u100))
(define-constant ERR_INVALID_SIGNATURE (err u101))
(define-constant ERR_INSUFFICIENT_BALANCE (err u102))
(define-constant ERR_TIME_LOCKED (err u103))
(define-constant ERR_NO_VAULT (err u104))
(define-constant ERR_INVALID_NONCE (err u105))
(define-constant ERR_PUBKEY_EXISTS (err u106))
(define-constant ERR_NO_PUBKEY (err u107))
(define-constant ERR_UNTRUSTED_CONTRACT (err u108))
(define-constant ERR_CONTRACT_HASH_MISMATCH (err u109))
(define-constant ERR_TRANSFER_FAILED (err u110))
(define-constant ERR_INVALID_AMOUNT (err u111))

;; ============================================
;; DATA MAPS & VARS
;; ============================================

;; User Vaults: stores pubkey, balance, time-lock, and nonce
(define-map user-vaults principal 
  {
    pubkey: (buff 33),             ;; Compressed secp256k1 public key
    balance: uint,                  ;; STX balance in microstacks
    time-lock-height: uint,         ;; Block height for unlock
    nonce: uint                     ;; Replay protection
  }
)

;; Trusted Contracts: stores expected code hashes (principal -> expected-hash)
(define-map trusted-contracts principal (buff 32))

;; Global Stats
(define-data-var total-deposits uint u0)
(define-data-var total-vaults uint u0)
(define-data-var total-withdrawals uint u0)

;; ============================================
;; SECP256K1 SIGNATURE VERIFICATION
;; Standard Bitcoin/Stacks signature verification
;; ============================================

(define-private (verify-signature 
    (user principal) 
    (message-hash (buff 32)) 
    (signature (buff 65)))
  (let (
    (vault (unwrap! (map-get? user-vaults user) false))
    (pubkey (get pubkey vault))
    ;; Recover the public key from the signature
    (recovered (secp256k1-recover? message-hash signature))
  )
    ;; Compare recovered pubkey with stored pubkey
    ;; secp256k1-recover? returns (response (buff 33) uint)
    (match recovered
      recovered-key (is-eq recovered-key pubkey)
      err-code false
    )
  )
)

;; ============================================
;; STX-ACCOUNT FEATURE
;; Query detailed STX account information
;; ============================================

(define-read-only (get-account-info (user principal))
  ;; Get locked/unlocked STX breakdown
  (let (
    (account (stx-account user))
  )
    {
      locked: (get locked account),
      unlock-height: (get unlock-height account),
      unlocked: (get unlocked account),
      total: (+ (get locked account) (get unlocked account))
    }
  )
)

;; ============================================
;; TIME-BASED LOGIC
;; Using burn-block-height for time locks
;; ============================================

(define-read-only (get-current-block-height)
  burn-block-height
)

(define-read-only (is-time-locked (user principal))
  (match (map-get? user-vaults user)
    vault (> (get time-lock-height vault) burn-block-height)
    false
  )
)

;; ============================================
;; CONTRACT VALIDATION
;; Validate external contracts before interaction
;; ============================================

;; NOTE: contract-code-hash? is available in Clarity 3.1+
;; For compatibility, we use a hash registry pattern
(define-read-only (get-trusted-hash (contract-principal principal))
  (map-get? trusted-contracts contract-principal)
)

(define-read-only (is-trusted-contract (contract-principal principal))
  (is-some (map-get? trusted-contracts contract-principal))
)

;; ============================================
;; PUBLIC FUNCTIONS
;; ============================================

;; Register a public key for the user's vault
(define-public (register-pubkey (pubkey (buff 33)))
  (let (
    (sender tx-sender)
  )
    ;; Check if vault already exists
    (asserts! (is-none (map-get? user-vaults sender)) ERR_PUBKEY_EXISTS)
    
    ;; Create new vault
    (map-set user-vaults sender {
      pubkey: pubkey,
      balance: u0,
      time-lock-height: u0,
      nonce: u0
    })
    
    ;; Update stats
    (var-set total-vaults (+ (var-get total-vaults) u1))
    
    (print {
      event: "pubkey-registered",
      user: sender,
      block-height: burn-block-height
    })
    
    (ok true)
  )
)

;; Deposit STX into the vault
(define-public (deposit (amount uint))
  (let (
    (sender tx-sender)
    (vault (unwrap! (map-get? user-vaults sender) ERR_NO_VAULT))
    (current-balance (get balance vault))
    (contract-addr (as-contract tx-sender))
  )
    ;; Validate amount
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    
    ;; Transfer STX to contract
    (unwrap! (stx-transfer? amount sender contract-addr) ERR_TRANSFER_FAILED)
    
    ;; Update vault balance
    (map-set user-vaults sender (merge vault {
      balance: (+ current-balance amount)
    }))
    
    ;; Update global stats
    (var-set total-deposits (+ (var-get total-deposits) amount))
    
    (print {
      event: "deposit",
      user: sender,
      amount: amount,
      new-balance: (+ current-balance amount),
      block-height: burn-block-height
    })
    
    (ok (+ current-balance amount))
  )
)

;; Withdraw STX with signature verification
(define-public (withdraw 
    (amount uint)
    (signature (buff 65))
    (nonce uint))
  (let (
    (sender tx-sender)
    (vault (unwrap! (map-get? user-vaults sender) ERR_NO_VAULT))
    (current-balance (get balance vault))
    (current-nonce (get nonce vault))
    (time-lock-height (get time-lock-height vault))
    ;; Create message hash: user + amount + nonce
    (message-hash (keccak256 (concat 
      (unwrap-panic (to-consensus-buff? sender))
      (concat 
        (unwrap-panic (to-consensus-buff? amount))
        (unwrap-panic (to-consensus-buff? nonce))
      )
    )))
  )
    ;; Validate amount
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    (asserts! (<= amount current-balance) ERR_INSUFFICIENT_BALANCE)
    
    ;; Check time-lock using block height
    (asserts! (<= time-lock-height burn-block-height) ERR_TIME_LOCKED)
    
    ;; Validate nonce (replay protection)
    (asserts! (is-eq nonce current-nonce) ERR_INVALID_NONCE)
    
    ;; Verify signature (secp256k1)
    (asserts! (verify-signature sender message-hash signature) ERR_INVALID_SIGNATURE)
    
    ;; Transfer STX to user
    (unwrap! (as-contract (stx-transfer? amount tx-sender sender)) ERR_TRANSFER_FAILED)
    
    ;; Update vault
    (map-set user-vaults sender (merge vault {
      balance: (- current-balance amount),
      nonce: (+ current-nonce u1)
    }))
    
    ;; Update stats
    (var-set total-withdrawals (+ (var-get total-withdrawals) amount))
    
    (print {
      event: "withdrawal",
      user: sender,
      amount: amount,
      remaining-balance: (- current-balance amount),
      nonce: (+ current-nonce u1),
      block-height: burn-block-height
    })
    
    (ok (- current-balance amount))
  )
)

;; Set a time-lock on the vault (delayed withdrawals)
(define-public (set-time-lock (unlock-height uint))
  (let (
    (sender tx-sender)
    (vault (unwrap! (map-get? user-vaults sender) ERR_NO_VAULT))
  )
    ;; Use block height for time-based logic
    (asserts! (> unlock-height burn-block-height) ERR_INVALID_AMOUNT)
    
    ;; Update time-lock
    (map-set user-vaults sender (merge vault {
      time-lock-height: unlock-height
    }))
    
    (print {
      event: "time-lock-set",
      user: sender,
      unlock-at: unlock-height,
      current-height: burn-block-height
    })
    
    (ok unlock-height)
  )
)

;; Add a trusted contract (admin only)
(define-public (add-trusted-contract (contract-principal principal) (expected-hash (buff 32)))
  (begin
    ;; Only contract owner can add trusted contracts
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    
    ;; Store the expected code hash
    (map-set trusted-contracts contract-principal expected-hash)
    
    (print {
      event: "trusted-contract-added",
      contract: contract-principal,
      code-hash: expected-hash,
      block-height: burn-block-height
    })
    
    (ok expected-hash)
  )
)

;; Remove trusted contract
(define-public (remove-trusted-contract (contract-principal principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (map-delete trusted-contracts contract-principal)
    
    (print {
      event: "trusted-contract-removed",
      contract: contract-principal,
      block-height: burn-block-height
    })
    
    (ok true)
  )
)

;; Emergency withdrawal (owner only, for stuck funds)
(define-public (emergency-withdraw (amount uint) (recipient principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_NOT_AUTHORIZED)
    (asserts! (> amount u0) ERR_INVALID_AMOUNT)
    
    (unwrap! (as-contract (stx-transfer? amount tx-sender recipient)) ERR_TRANSFER_FAILED)
    
    (print {
      event: "emergency-withdrawal",
      amount: amount,
      recipient: recipient,
      block-height: burn-block-height
    })
    
    (ok amount)
  )
)

;; ============================================
;; READ-ONLY FUNCTIONS
;; ============================================

(define-read-only (get-vault (user principal))
  (map-get? user-vaults user)
)

(define-read-only (get-balance (user principal))
  (match (map-get? user-vaults user)
    vault (ok (get balance vault))
    ERR_NO_VAULT
  )
)

(define-read-only (get-nonce (user principal))
  (match (map-get? user-vaults user)
    vault (ok (get nonce vault))
    ERR_NO_VAULT
  )
)

(define-read-only (get-time-lock-height (user principal))
  (match (map-get? user-vaults user)
    vault (ok (get time-lock-height vault))
    ERR_NO_VAULT
  )
)

(define-read-only (get-total-deposits)
  (var-get total-deposits)
)

(define-read-only (get-total-withdrawals)
  (var-get total-withdrawals)
)

(define-read-only (get-total-vaults)
  (var-get total-vaults)
)

(define-read-only (get-contract-balance)
  (stx-get-balance (as-contract tx-sender))
)

;; ============================================
;; MESSAGE HASH HELPER (for off-chain signing)
;; ============================================

(define-read-only (get-withdrawal-message-hash 
    (user principal) 
    (amount uint) 
    (nonce uint))
  (keccak256 (concat 
    (unwrap-panic (to-consensus-buff? user))
    (concat 
      (unwrap-panic (to-consensus-buff? amount))
      (unwrap-panic (to-consensus-buff? nonce))
    )
  ))
)

;; ============================================
;; UTILITY FUNCTIONS
;; ============================================

(define-read-only (get-contract-owner)
  CONTRACT_OWNER
)

Functions (23)

FunctionAccessArgs
verify-signatureprivateuser: principal, message-hash: (buff 32
get-account-inforead-onlyuser: principal
get-current-block-heightread-only
is-time-lockedread-onlyuser: principal
get-trusted-hashread-onlycontract-principal: principal
is-trusted-contractread-onlycontract-principal: principal
register-pubkeypublicpubkey: (buff 33
depositpublicamount: uint
withdrawpublicamount: uint, signature: (buff 65
set-time-lockpublicunlock-height: uint
add-trusted-contractpubliccontract-principal: principal, expected-hash: (buff 32
remove-trusted-contractpubliccontract-principal: principal
emergency-withdrawpublicamount: uint, recipient: principal
get-vaultread-onlyuser: principal
get-balanceread-onlyuser: principal
get-nonceread-onlyuser: principal
get-time-lock-heightread-onlyuser: principal
get-total-depositsread-only
get-total-withdrawalsread-only
get-total-vaultsread-only
get-contract-balanceread-only
get-withdrawal-message-hashread-onlyuser: principal, amount: uint, nonce: uint
get-contract-ownerread-only