Learn-skills.dev clarity-patterns

Clarity smart contract pattern library — reusable code patterns, contract templates, and design references for building on Stacks.

install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/aibtcdev/skills/clarity-patterns" ~/.claude/skills/neversight-learn-skills-dev-clarity-patterns && rm -rf "$T"
manifest: data/skills-md/aibtcdev/skills/clarity-patterns/SKILL.md
source content

Clarity Patterns Skill

Canonical pattern library for Clarity smart contract development on Stacks. All patterns and templates are bundled in this skill — no external dependencies.

This is a doc-only skill. Agents read this file and the colocated reference files directly. The CLI interface documents the planned implementation.

bun run clarity-patterns/clarity-patterns.ts <subcommand> [options]

Subcommands

  • list [--category <category>]
    — List available patterns and templates (categories:
    code
    ,
    registry
    ,
    templates
    ,
    testing
    )
  • get --name <pattern-name>
    — Return a specific pattern with code and notes
  • template --name <template-name>
    — Return a complete contract template with source, tests, and checklist

Code Patterns

Public Function Template

Standard structure for public functions with guards and error handling.

(define-public (transfer (amount uint) (to principal))
  (begin
    (asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED)
    (try! (ft-transfer? TOKEN amount tx-sender to))
    (ok true)))
  • Use
    try!
    for subcalls to propagate errors
  • Use
    asserts!
    for guards before state changes
  • Add post-conditions on tx for asset safety

Standardized Events

Emit structured events for off-chain indexing.

(print {
  notification: "contract-event",
  payload: {
    amount: amount,
    sender: tx-sender,
    recipient: to
  }
})

Error Handling with Match

Handle external call failures gracefully.

(match (contract-call? .other fn args)
  success (ok success)
  error (err ERR_EXTERNAL_CALL_FAILED))

Bit Flags for Status/Permissions

Pack multiple booleans into a single uint.

(define-constant STATUS_ACTIVE (pow u2 u0))   ;; 1
(define-constant STATUS_PAID (pow u2 u1))     ;; 2
(define-constant STATUS_VERIFIED (pow u2 u2)) ;; 4

;; Pack multiple flags: (+ STATUS_ACTIVE STATUS_PAID) → u3
;; Check flag: (> (bit-and status STATUS_ACTIVE) u0)
;; Set flag: (var-set status (bit-or (var-get status) NEW_FLAG))
;; Clear flag: (var-set status (bit-and (var-get status) (bit-not FLAG)))

Examples: aibtc-action-proposal-voting

Multi-Send Pattern

Send to multiple recipients in one transaction using fold.

(define-private (send-maybe
    (recipient {to: principal, ustx: uint})
    (prior (response bool uint)))
  (match prior
    ok-result (let (
      (to (get to recipient))
      (ustx (get ustx recipient)))
      (try! (stx-transfer? ustx tx-sender to))
      (ok true))
    err-result (err err-result)))

(define-public (send-many (recipients (list 200 {to: principal, ustx: uint})))
  (fold send-maybe recipients (ok true)))

Parent-Child Maps (Hierarchical Data)

Store hierarchical data with pagination support.

(define-map Parents uint {name: (string-ascii 32), lastChildId: uint})
(define-map Children {parentId: uint, id: uint} uint)

(define-read-only (get-child (parentId uint) (childId uint))
  (map-get? Children {parentId: parentId, id: childId}))

(define-private (is-some? (x (optional uint)))
  (is-some x))

(define-read-only (get-children (parentId uint) (shift uint))
  (filter is-some?
    (list
      (get-child parentId (+ shift u1))
      (get-child parentId (+ shift u2))
      (get-child parentId (+ shift u3))
      ;; ... up to page size
    )))

Whitelisting (Assets/Contracts)

Control which contracts/assets can interact.

(define-map Allowed {contract: principal, type: uint} bool)

;; Check in function
(asserts! (default-to false (map-get? Allowed {contract: contract, type: type}))
          ERR_NOT_ALLOWED)

;; Batch update
(define-public (set-allowed-list (items (list 100 {token: principal, enabled: bool})))
  (ok (map set-iter items (ok true))))

Examples: ccd002-treasury-v3, aibtc-agent-account

Trait Whitelisting

Only allow calls from trusted trait implementations.

(define-map TrustedTraits principal bool)

;; In functions accepting traits
(asserts! (default-to false (map-get? TrustedTraits (contract-of t)))
          ERR_UNTRUSTED)

Delayed Activation

Activate functionality after a Bitcoin block delay.

(define-constant DELAY u21000) ;; ~146 days in BTC blocks
(define-data-var activation-block uint u0)

;; Set on deploy or init
(var-set activation-block (+ burn-block-height DELAY))

(define-read-only (is-active?)
  (>= burn-block-height (var-get activation-block)))

Example: usabtc-token

Rate Limiting

Prevent rapid repeated actions.

(define-data-var last-action-block uint u0)

(define-public (rate-limited-action)
  (begin
    (asserts! (> burn-block-height (var-get last-action-block)) ERR_RATE_LIMIT)
    (var-set last-action-block burn-block-height)
    ;; ... action
    (ok true)))

DAO Proposals with Historic Balances

Use

at-block
for snapshot voting.

(define-map Proposals uint {
  votesFor: uint,
  votesAgainst: uint,
  status: uint,
  liquidTokens: uint,
  blockHash: (buff 32)
})

;; Get voting power at proposal creation
(define-read-only (get-vote-power (proposal-id uint) (voter principal))
  (let ((proposal (unwrap! (map-get? Proposals proposal-id) u0)))
    (at-block (get blockHash proposal)
      (contract-call? .token get-balance voter))))

;; Quorum check: (>= (/ (* total-votes u100) liquid-supply) QUORUM_PERCENT)

Example: aibtc-action-proposal-voting

Fixed-Point Arithmetic

Handle decimal values with scale factor.

(define-constant SCALE (pow u10 u8)) ;; 8 decimal places

;; Multiply then divide to preserve precision
(define-read-only (calculate-share (amount uint) (percentage uint))
  (/ (* amount percentage) SCALE))

;; Convert to/from scaled values
(define-read-only (to-scaled (amount uint))
  (* amount SCALE))

(define-read-only (from-scaled (amount uint))
  (/ amount SCALE))

Example: ccd012-redemption-nyc

Treasury Pattern with as-contract

Use

as-contract
for contract-controlled funds.

(define-public (withdraw (amount uint) (recipient principal))
  (begin
    (asserts! (is-authorized tx-sender) ERR_UNAUTHORIZED)
    (as-contract (stx-transfer? amount (as-contract tx-sender) recipient))))

Warning:

as-contract
changes both
tx-sender
and
contract-caller
to the contract principal.

tx-sender vs contract-caller Decision Framework

Call Pathcontract-callertx-sender
user -> targetuseruser
user -> proxy -> targetproxyuser
user -> proxy (as-contract) -> targetproxyproxy
  • tx-sender: Use for auth checks, identity attribution, self-action guards. Preserves human identity through normal proxies. Preferred for composability.
  • contract-caller: Use when you need the IMMEDIATE caller identity specifically.
  • Security note: Using
    contract-caller
    for self-action guards (e.g., "owner can't give themselves feedback") is bypassable — owner routes through any proxy and
    contract-caller
    shows the proxy, not the owner.
    tx-sender
    catches this because it preserves the human origin.

Examples: ccd002-treasury-v3, aibtc-agent-account

Clarity 4: Asset Restrictions

Restrict what assets a contract call can move.

(as-contract
  (with-stx u1000000)                              ;; Allow 1 STX
  (with-ft .token TOKEN u500)                      ;; Allow 500 fungible tokens
  (with-nft .nft-contract NFT (list u1 u2 u3))    ;; Allow specific NFT IDs
  ;; ... body
)

;; DANGER: Avoid unless necessary
(with-all-assets-unsafe)

Multi-Party Coordination

Coordinate actions requiring multiple signatures.

;; Proposal state
(define-map Intents uint {
  participants: (list 20 principal),
  accepts: uint,     ;; Bitmask of who accepted
  status: uint,      ;; 0=pending, 1=ready, 2=executed, 3=cancelled
  expiry: uint,
  payload: (buff 256)
})

;; Accept via signature verification
(define-public (accept (intent-id uint) (signature (buff 65)))
  (let (
    (intent (unwrap! (map-get? Intents intent-id) ERR_NOT_FOUND))
    (msg-hash (sha256 (concat (int-to-ascii intent-id) (get payload intent))))
    (signer (try! (secp256k1-recover? msg-hash signature))))
    ;; Verify signer is participant, update accepts bitmask
    (ok true)))

Reference: ERC-8001 pattern for decidable multi-party coordination.


Registry Patterns

Block Snapshot Pattern

Capture comprehensive chain state at transaction time. This is the "receipt" that makes a transaction worth the fee.

;; Full snapshot — comprehensive (use for high-value records)
(define-private (capture-snapshot)
  {
    stacksBlock: stacks-block-height,
    burnBlock: burn-block-height,
    tenure: tenure-height,
    blockTime: stacks-block-time,
    chainId: chain-id,
    txSender: tx-sender,
    contractCaller: contract-caller,
    txSponsor: tx-sponsor?,
    stacksBlockHash: (get-stacks-block-info? id-header-hash (- stacks-block-height u1)),
    burnBlockHash: (get-burn-block-info? header-hash (- burn-block-height u1))
  }
)

;; Standard snapshot — balanced cost
;; {stacksBlock, burnBlock, blockTime, txSender}

;; Minimal snapshot — cheapest
;; {stacksBlock, burnBlock}

Previous block hashes are captured because the current block's hash isn't finalized until after the transaction. The previous block's hash is immutable and independently verifiable.

Principal-Keyed Registry

Track state per address (heartbeats, profiles, balances). One entry per address, overwrites on subsequent calls.

(define-map Registry
  principal
  {
    stacksBlock: uint,
    burnBlock: uint,
    count: uint
  }
)

(map-get? Registry address)
(map-set Registry tx-sender {...})

Hash-Keyed Registry

Track unique data (attestations, commitments). One entry per unique hash, first-write-wins.

(define-map Registry
  (buff 32)
  {
    attestor: principal,
    stacksBlock: uint
  }
)

;; First attestor wins
(asserts! (is-none (map-get? Registry hash)) ERR_ALREADY_EXISTS)
(map-set Registry hash {...})

Composite-Keyed Registry

Multi-dimensional tracking (votes per proposal, actions per agent).

(define-map Registry
  {entity: principal, action: uint}
  {stacksBlock: uint}
)

(map-get? Registry {entity: address, action: action-id})

Secondary Index Pattern

Enable enumeration of entries by address when primary key isn't the address.

;; Primary: hash -> data
(define-map Attestations (buff 32) {...})

;; Secondary: address + index -> hash
(define-map AttestorIndex
  {attestor: principal, index: uint}
  (buff 32)
)

;; Counter for next index
(define-map AttestorCount principal uint)

;; On insert:
(let ((idx (default-to u0 (map-get? AttestorCount attestor))))
  (map-set AttestorIndex {attestor: attestor, index: idx} hash)
  (map-set AttestorCount attestor (+ idx u1)))

;; Enumerate:
(define-read-only (get-attestor-hash-at (attestor principal) (index uint))
  (map-get? AttestorIndex {attestor: attestor, index: index}))

Global Stats Pattern

Track aggregate metrics without iterating.

(define-data-var totalEntries uint u0)
(define-data-var uniqueAddresses uint u0)

;; On new entry:
(var-set totalEntries (+ (var-get totalEntries) u1))
(if isNewAddress
  (var-set uniqueAddresses (+ (var-get uniqueAddresses) u1))
  true)

;; Read stats:
(define-read-only (get-stats)
  {
    totalEntries: (var-get totalEntries),
    uniqueAddresses: (var-get uniqueAddresses)
  }
)

Write Semantics

First-Write-Wins (Attestations):

(define-public (attest (key (buff 32)))
  (begin
    (asserts! (is-none (map-get? Registry key)) ERR_ALREADY_EXISTS)
    (map-set Registry key {...})
    (ok true)))

Last-Write-Wins (Heartbeats):

(define-public (check-in)
  (begin
    (map-set Registry tx-sender {...})
    (ok true)))

Append-Only (History):

(define-map History
  {address: principal, index: uint}
  {...snapshot...}
)

(define-public (record)
  (let ((idx (default-to u0 (map-get? HistoryCount tx-sender))))
    (map-set History {address: tx-sender, index: idx} {...})
    (map-set HistoryCount tx-sender (+ idx u1))
    (ok idx)))

Access Control Patterns

Open (anyone can write):

(define-public (register)
  (ok (map-set Registry tx-sender {...})))

Self-Only (registered users update own entries):

(define-public (update (data (buff 64)))
  (begin
    (asserts! (is-some (map-get? Registry tx-sender)) ERR_NOT_REGISTERED)
    (map-set Registry tx-sender {...})
    (ok true)))

Admin-Gated:

(define-data-var admin principal CONTRACT_OWNER)

(define-public (register-address (address principal))
  (begin
    (asserts! (is-eq tx-sender (var-get admin)) ERR_UNAUTHORIZED)
    (map-set Registry address {...})
    (ok true)))

Testing Reference

Testing Pyramid

Stxer (Historical Simulation)      — Mainnet fork, pre-deployment validation
RV (Property-Based Fuzzing)        — Invariants, edge cases, battle-grade
Vitest + Clarinet SDK              — Integration tests, TypeScript
Clarunit                           — Unit tests in Clarity itself

When to Use Each Tool

ToolUse WhenSkip When
Clarinet SDKStandard testing, CI/CD, type-safe-
ClarunitTesting Clarity logic in Clarity, simple assertionsComplex multi-account flows
RVTreasuries, DAOs, high-value contracts, finding edge casesSimple contracts, time pressure
StxerPre-mainnet validation, governance simulationsEarly development, testnet-only

Vitest Config

import { defineConfig } from "vitest/config";
import { vitestSetupFilePath, getClarinetVitestsArgv } from "@hirosystems/clarinet-sdk/vitest";

export default defineConfig({
  test: {
    environment: "clarinet",
    singleThread: true,
    setupFiles: [vitestSetupFilePath],
    environmentOptions: {
      clarinet: getClarinetVitestsArgv(),
    },
  },
});

Test Structure (Arrange-Act-Assert)

import { Cl } from "@stacks/transactions";
import { describe, expect, it } from "vitest";

describe("my-contract", function () {
  it("transfers tokens correctly", function () {
    // ARRANGE
    const deployer = simnet.deployer;
    const wallet1 = simnet.getAccounts().get("wallet_1")!;
    const amount = 100;

    // ACT
    const result = simnet.callPublicFn(
      "my-contract",
      "transfer",
      [Cl.uint(amount), Cl.principal(wallet1)],
      deployer
    );

    // ASSERT
    expect(result.result).toBeOk(Cl.bool(true));
  });
});

Key Gotchas

  • NO
    beforeAll
    /
    beforeEach
    — simnet resets each test file session
  • Single thread required
    singleThread: true
    for simnet isolation
  • Functions over arrow functions for test helpers
  • Use
    cvToValue()
    and
    cvToJSON()
    for Clarity-to-JS conversion

Clarity Value Constructors

import { Cl, cvToValue, cvToJSON } from "@stacks/transactions";

Cl.uint(100)                           // uint
Cl.int(-50)                            // int
Cl.bool(true)                          // bool
Cl.principal("SP123...")               // principal
Cl.contractPrincipal("SP123", "name")  // contract principal
Cl.stringAscii("hello")               // (string-ascii N)
Cl.stringUtf8("hello")                // (string-utf8 N)
Cl.bufferFromHex("deadbeef")           // (buff N)
Cl.tuple({ amount: Cl.uint(100) })     // tuple
Cl.list([Cl.uint(1), Cl.uint(2)])      // list
Cl.some(Cl.uint(100))                  // (some value)
Cl.none()                              // none

Custom Matchers

expect(result.result).toBeOk(Cl.uint(100));
expect(result.result).toBeErr(Cl.uint(1));
expect(result.result).toBeBool(true);
expect(result.result).toBeUint(100);
expect(result.result).toBePrincipal("SP123...");

RV (Rendezvous) Fuzz Tests

;; Property: loan amount always increases correctly
(define-public (test-borrow (amount uint))
  (if (is-eq amount u0)
    (ok false)  ;; Discard invalid input
    (let ((initial (get-loan tx-sender)))
      (try! (borrow amount))
      (asserts! (is-eq (get-loan tx-sender) (+ initial amount))
        (err u999))
      (ok true))))

;; Invariant: total supply never exceeds cap
(define-read-only (invariant-supply-capped)
  (<= (var-get total-supply) MAX_SUPPLY))

Run:

npx rv . my-contract test
(properties),
npx rv . my-contract invariant
(invariants)

Clarunit Tests

;; @name Multiplication works correctly
(define-public (test-multiply)
  (begin
    (asserts! (is-eq u8 (contract-call? .math multiply u2 u4))
      (err "2 * 4 should equal 8"))
    (ok true)))

File:

tests/my-contract_test.clar
, functions start with
test-
.

Project Structure (Full Stack)

my-project/
├── Clarinet.toml
├── vitest.config.js
├── package.json
├── contracts/
│   ├── my-contract.clar
│   └── my-contract.tests.clar      # RV tests
├── tests/
│   ├── my-contract.test.ts         # Vitest
│   ├── my-contract_test.clar       # Clarunit
│   └── clarunit.test.ts            # Clarunit runner
└── simulations/
    └── my-contract-stxer.ts        # Stxer

package.json Scripts

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:rv": "npx rv . my-contract test",
    "test:rv:invariant": "npx rv . my-contract invariant",
    "test:stxer": "npx tsx simulations/my-contract-stxer.ts"
  }
}

Contract Templates

Full contract templates with source and tests are in colocated files:

TemplateFileDescription
heartbeat-registry
templates/heartbeat-registry.mdAgent heartbeat with full chain context, address enumeration, liveness checks
proof-of-existence
templates/proof-of-existence.mdDocument timestamping with SIP-018 signatures, first-write-wins, attestor index
registry-minimal
templates/registry-minimal.mdMinimal registry combining snapshot + stats + events

Execution Cost Limits

CategoryBlock LimitRead-Only Limit
Runtime5,000,000,0001,000,000,000
Read count15,00030
Read bytes100,000,000100,000
Write count15,0000
Write bytes15,000,0000

Cost Optimization Tips

  1. Inline single-use values — avoid unnecessary
    let
    bindings
  2. Constants over data-vars — constants are cheaper to read
  3. Bulk operations — single call with list beats multiple calls
  4. Separate params vs tuples — flat params are cheaper for function calls
  5. Off-chain computation — move non-essential logic to UI/indexer

Quick Reference

  • Use
    stacks-block-height
    not
    block-height
    (deprecated)
  • Use
    tx-sender
    for token operations,
    contract-caller
    only when immediate caller identity is needed
  • Use
    try!
    for error propagation,
    asserts!
    for guards before state changes
  • All public functions must return
    (response ok err)
  • Error codes should be unique constants
  • Events:
    {notification: "event-name", payload: {...}}
    format
  • Check costs with
    ::get_costs
    in clarinet console

References