Clarity 智慧合約語言

理解 Stacks 的 Clarity 語言設計理念與安全性。

Clarity 智慧合約語言:完整開發指南

理解 Stacks 的 Clarity 語言設計理念與安全性。Clarity 是一種圖靈不完備的智慧合約語言,專為比特幣生態系統設計,採用可判定的執行模型,確保合約行為的可預測性和可驗證性。

Clarity 的可判定特性非常適合高審計需求合約。它不是追求圖靈完備,而是追求可預測、可驗證、可審計。這種設計選擇使其成為比特幣上構建金融應用的理想選擇。

Clarity 語言核心特性

為何選擇圖靈不完備

圖靈不完備的設計優勢:

1. 可判定性
   - 執行前可確定最大 Gas 消耗
   - 避免無限迴圈攻擊
   - 合約成本可精確預測

2. 安全性
   - 無法創建自毀合約
   - 無法動態調用任意合約
   - 減少攻擊向量

3. 可審計性
   - 所有函數行為可在部署前分析
   - 無隱藏執行路徑
   - 形式化驗證更容易

資料類型系統

Clarity 採用強類型系統,所有變數必須明確宣告類型:

Clarity 基本類型:

────────────────────────────
類型         說明         範例
────────────────────────────
int          有符號整數   42
uint         無符號整數   123
bool         布林值       true
buff         位元組       0x01
string       字串         "hello"
principal    主體位址     ST1...
(list t)     清單         (list 1 2 3)
(tuple t)    結構體       {a: 1, b: 2}
(optional t) 可選值       (some 42)
(response t) 回應類型     (ok 42)
              (err "error")
────────────────────────────

完整合約範例:簡單代幣合約

以下是一個完整的 RGB20 風格代幣合約,展示 Clarity 的實際開發:

;; Simple Token Contract
;; 相當於 ERC-20 的 Clarity 實現

;; ============== 常量定義 ==============
(define-constant CONTRACT_OWNER tx-sender)
(define-constant ERR_OWNER_ONLY (err u100))
(define-constant ERR_INSUFFICIENT_BALANCE (err u101))
(define-constant ERR_TRANSFER_FAILED (err u102))
(define-constant ERR_NOT_AUTHORIZED (err u103))
(define-constant MAX_SUPPLY u21000000)
(define-constant DECIMALS u8)

;; ============== 資料儲存 ==============
(define-map balances principal uint)
(define-map allowances (tuple (owner principal) (spender principal)) uint)
(define-data-var total-supply uint u0)
(define-data-var token-name (string-ascii 32) "BitcoinFi Token")
(define-data-var token-symbol (string-ascii 10) "BFT")

;; ============== 讀取函數 ==============

(define-read-only (get-name)
  (ok (var-get token-name)))

(define-read-only (get-symbol)
  (ok (var-get token-symbol)))

(define-read-only (get-decimals)
  (ok DECIMALS))

(define-read-only (get-total-supply)
  (ok (var-get total-supply)))

(define-read-only (get-balance (account principal))
  (default-to u0 (map-get? balances account)))

(define-read-only (get-allowance (owner principal) (spender principal))
  (default-to u0 (map-get? allowances {owner: owner, spender: spender})))

;; ============== 內部函數 ==============

(define-private (transfer-internal (amount uint) (from principal) (to principal))
  (let (
    (from-balance (get-balance from))
  )
    (asserts! (>= from-balance amount) ERR_INSUFFICIENT_BALANCE)
    (map-set balances from (- from-balance amount))
    (map-set balances to (+ (get-balance to) amount))
    (ok true)
  )
))

;; ============== 公開函數 ==============

;; 鑄造代幣(僅合約擁有者)
(define-public (mint (amount uint) (to principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_OWNER_ONLY)
    (var-set total-supply (+ (var-get total-supply) amount))
    (map-set balances to (+ (get-balance to) amount))
    (ok amount)
  )
)

;; 銷毀代幣
(define-public (burn (amount uint))
  (let (
    (sender-balance (get-balance tx-sender))
  )
    (asserts! (>= sender-balance amount) ERR_INSUFFICIENT_BALANCE)
    (var-set total-supply (- (var-get total-supply) amount))
    (map-set balances tx-sender (- sender-balance amount))
    (ok amount)
  )
)

;; 轉帳
(define-public (transfer (amount uint) (to principal))
  (transfer-internal amount tx-sender to)
)

;; 授權轉帳
(define-public (approve (spender principal) (amount uint))
  (begin
    (map-set allowances {owner: tx-sender, spender: spender} amount)
    (ok amount)
  )
)

;; 從授權額度轉帳
(define-public (transfer-from (amount uint) (from principal) (to principal))
  (let (
    (allowance-amount (get-allowance from tx-sender))
  )
    (asserts! (>= allowance-amount amount) ERR_NOT_AUTHORIZED)
    (try! (transfer-internal amount from to))
    (map-set allowances {owner: from, spender: tx-sender}
      (- allowance-amount amount))
    (ok amount)
  )
)

;; ============== 事件(可選) ==============
;; Clarity 不支持事件,但可通過鑄造 NFT 實現類似功能

多重簽名合約範例

展示如何使用 Clarity 實現 2-of-3 多重簽名:

;; Multi-Signature Wallet Contract

(define-constant ERR_NOT_SIGNER (err u100))
(define-constant ERR_ALREADY_SIGNED (err u101))
(define-constant ERR_INSUFFICIENT_SIGNATURES (err u102))
(define-constant ERR_INVALID_THRESHOLD (err u103))

;; 閾值:需要多少個簽名
(define-data-var threshold uint u2)

;; 授權者清單
(define-list signers-list (list
  #principal with address 1
  #principal with address 2
  #principal with address 3
))

;; 待處理的交易
(define-map pending-txs
  uint
  {
    recipient: principal,
    amount: uint,
    executed: bool,
    signer-count: uint
  }
)

;; 交易簽名記錄
(define-map tx-signatures
  (tuple (tx-id uint) (signer principal))
  bool
)

;; 交易計數器
(define-data-var tx-counter uint u0)

;; 內部:檢查是否為授權者
(define-private (is-signer (principal principal))
  (is-some (index-of signers-list principal))
)

;; 建立交易
(define-public (create-transaction (recipient principal) (amount uint))
  (let (
    (tx-id (var-get tx-counter))
  )
    (asserts! (is-signer tx-sender) ERR_NOT_SIGNER)
    (map-set pending-txs tx-id {
      recipient: recipient,
      amount: amount,
      executed: false,
      signer-count: u0
    })
    (var-set tx-counter (+ (var-get tx-counter) u1))
    (ok tx-id)
  )
)

;; 簽名交易
(define-public (sign-transaction (tx-id uint))
  (let (
    (tx (unwrap! (map-get? pending-txs tx-id) ERR_INVALID_THRESHOLD))
  )
    (asserts! (is-signer tx-sender) ERR_NOT_SIGNER)
    (asserts! (not (is-some (map-get? tx-signatures {tx-id: tx-id, signer: tx-sender}))) ERR_ALREADY_SIGNED)
    (map-set tx-signatures {tx-id: tx-id, signer: tx-sender} true)
    (map-set pending-txs tx-id (merge tx {
      signer-count: (+ (get signer-count tx) u1)
    }))
    (ok (get signer-count tx))
  )
)

;; 執行交易
(define-public (execute-transaction (tx-id uint))
  (let (
    (tx (unwrap! (map-get? pending-txs tx-id) ERR_INVALID_THRESHOLD))
    (signer-count (get signer-count tx))
  )
    (asserts! (>= signer-count (var-get threshold)) ERR_INSUFFICIENT_SIGNATURES)
    (asserts! (not (get executed tx)) ERR_INVALID_THRESHOLD)
    (map-set pending-txs tx-id (merge tx {executed: true}))
    (ok true)
  )
)

可重現的實作流程

測試驅動開發

Clarity 支援合約內測試,可在部署前驗證邏輯正確性:

;; ============== 合約內測試 ==============

(define-public (test-transfer)
  (let (
    ;; 設置測試環境
    (user1 'ST1SJ3DVE5Q0P54RX3D5T7XY4X7G9K5Z5T4X7G9K5)
    (user2 'ST2CY5V39HJ8K8D7X5Z3T7XY4X7G9K5Z5T4X7G9K6)
    (initial-balance u1000)
    (transfer-amount u500)
  )
    ;; 初始化餘額
    (map-set balances user1 initial-balance)
    (map-set balances user2 u0)

    ;; 執行轉帳
    (try! (transfer transfer-amount user2))

    ;; 驗證結果
    (asserts! (= (get-balance user1) (- initial-balance transfer-amount))
      (err u999))
    (asserts! (= (get-balance user2) transfer-amount)
      (err u999))

    (ok "Test passed")
  )
)

本地測試框架

使用 clarinet 進行本地測試:

# 安裝 clarinet
cargo install clarinet

# 初始化專案
clarinet new my-token
cd my-token

# 編輯合約
# contracts/token.clar

# 單元測試
clarinet test

# 互動式控制台
clarinet console

# 檢查合約成本
clarinet check
// test/_token.test.ts
import { Clarinet, Tx, Chain, Account, types } from '@hirosystems/clarinet-sdk';

Clarinet.test({
  name: "transfer reduces sender balance",
  fn(chain: Chain, accounts: Map<string, Account>) {
    const deployer = accounts.get('deployer')!;
    const wallet1 = accounts.get('wallet_1')!;
    const wallet2 = accounts.get('wallet_2')!;

    // 鑄造代幣給 wallet1
    let block = chain.mineBlock([
      Tx.contractCall('token', 'mint', [
        types.uint(1000),
        types.principal(wallet1.address)
      ], deployer.address)
    ]);

    // 執行轉帳
    block = chain.mineBlock([
      Tx.contractCall('token', 'transfer', [
        types.uint(500),
        types.principal(wallet2.address)
      ], wallet1.address)
    ]);

    // 驗證餘額
    const wallet1Balance = chain.callReadOnlyFn(
      'token', 'get-balance', [types.principal(wallet1.address)]
    );
    const wallet2Balance = chain.callReadOnlyFn(
      'token', 'get-balance', [types.principal(wallet2.address)]
    );

    expect(wallet1Balance.result).toBe(types.uint(500));
    expect(wallet2Balance.result).toBe(types.uint(500));
  }
});

Clarity 與比特幣的整合

STX 轉帳與比特幣確認

Stacks 區塊鏈每個區塊都會等待比特幣確認,這提供了比特幣級的安全性:

;; ============== 比特幣確認整合 ==============

;; 讀取當前比特幣區塊高度
(define-public (get-btc-block-height)
  (ok block-height)
)

;; 根據比特幣確認數設定時間鎖
(define-public (create-timelocked-transfer
    (recipient principal)
    (amount uint)
    (btc-confirmations uint))
  (let (
    (unlock-height (+ block-height (* btc-confirmations u6)))
  )
    ;; 儲存時間鎖資訊
    (map-set time-locks recipient {
      amount: amount,
      unlock-height: unlock-height,
      claimed: false
    })
    (ok unlock-height)
  )
)

;; 領取時間鎖定的資金
(define-public (claim-timelocked ()
  (let (
    (lock (unwrap! (map-get? time-locks tx-sender) (err u404)))
  )
    (asserts! (>= block-height (get unlock-height lock))
      (err u100))
    (asserts! (not (get claimed lock))
      (err u101))
    (map-set time-locks tx-sender (merge lock {claimed: true}))
    (ok (get amount lock))
  )
)

SIP-010 代幣標準

Stacks 的 FT 標準介面:

;; SIP-010 Trait Definition
(define-trait sip010-token (
  ;; Transfer tokens from tx-sender to a recipient
  (transfer (uint principal principal) (response bool uint))

  ;; Get the token balance of an owner
  (get-balance (principal) (response uint uint))

  ;; Get the number of decimals
  (get-decimals () (response uint uint))

  ;; Get the token symbol
  (get-symbol () (response (string-ascii 10) uint))

  ;; Get the token name
  (get-name () (response (string-ascii 32) uint))

  ;; Get the total supply
  (get-total-supply () (response uint uint))

  ;; Get the token URI
  (get-token-uri () (response (optional (string-utf8 256)) uint))
))

部署與成本管理

Clarity 成本模型

Clarity 執行需要支付成本,主要來自以下操作:

成本因素分析:
─────────────────────────────
操作類型          成本單位
─────────────────────────────
儲存寫入         1 per byte
函數調用         1 per call
算術運算         1-10 per op
清單操作         O(n) per op
MAP 操作         O(log n)
─────────────────────────────

成本優化策略:

1. 批量操作
   ❌ 不推薦:
   (map-set map k1 v1)
   (map-set map k2 v2)
   (map-set map k3 v3)

   ✅ 推薦:
   (define-map map ...)
   (map-set map {k1: v1, k2: v2, k3: v3})

2. 減少儲存
   - 使用 integer 而非 map
   - 合併相關資料
   - 定期清理過期資料

3. 計算優化
   - 避免不必要的迴圈
   - 使用 early return
   - 快取重複計算

升級模式

Clarity 合約不可變,但可以通過代理模式實現升級:

;; 代理升級模式

;; 儲存當前實作位址
(define-data-var implementation principal .implementation-v1)

;; 委託調用
(define-public (forward (fn-name (string-oriascii 256)) (args (list 2000 (buff 1))))
  (contract-call? (var-get implementation) fn-name args)
)

;; 升級(僅擁有者)
(define-public (upgrade (new-implementation principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_OWNER_ONLY)
    (var-set implementation new-implementation)
    (ok true)
  )
)

例外情境:測試一定要覆蓋

設計取捨:成本、風險與維運的平衡點

Stacks 類題目需要把鏈上共識、跨鏈資產與應用風控一起評估。任何單點優化,若破壞整體一致性,都會在壓力情境中反噬。

如果你要把這篇主題直接導入現有服務,建議先做小流量灰度:

把 EVM 的動態寫法直接搬到 Clarity,通常會在語義與成本上踩雷。

重點回收

Clarity 的價值在可判定性,適合高審計需求場景。圖靈不完備的設計確保了:

  1. 執行成本可預測
  2. 無無限迴圈風險
  3. 形式化驗證可行

如果你要把本文主題用在生產環境,建議先完成「可重現測試、監控告警、失敗回滾」三件事,再擴大資金與流量。


相關文章

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!