Learn-skills.dev clarity-patterns
Clarity smart contract pattern library — reusable code patterns, contract templates, and design references for building on Stacks.
git clone https://github.com/NeverSight/learn-skills.dev
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"
data/skills-md/aibtcdev/skills/clarity-patterns/SKILL.mdClarity 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 available patterns and templates (categories:list [--category <category>]
,code
,registry
,templates
)testing
— Return a specific pattern with code and notesget --name <pattern-name>
— Return a complete contract template with source, tests, and checklisttemplate --name <template-name>
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
for subcalls to propagate errorstry! - Use
for guards before state changesasserts! - 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 } })
: string identifier for the event typenotification
: tuple with camelCase keyspayload- Examples: usabtc-token, ccd002-treasury-v3
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 Path | contract-caller | tx-sender |
|---|---|---|
| user -> target | user | user |
| user -> proxy -> target | proxy | user |
| user -> proxy (as-contract) -> target | proxy | proxy |
- 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
for self-action guards (e.g., "owner can't give themselves feedback") is bypassable — owner routes through any proxy andcontract-caller
shows the proxy, not the owner.contract-caller
catches this because it preserves the human origin.tx-sender
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
| Tool | Use When | Skip When |
|---|---|---|
| Clarinet SDK | Standard testing, CI/CD, type-safe | - |
| Clarunit | Testing Clarity logic in Clarity, simple assertions | Complex multi-account flows |
| RV | Treasuries, DAOs, high-value contracts, finding edge cases | Simple contracts, time pressure |
| Stxer | Pre-mainnet validation, governance simulations | Early 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
— simnet resets each test file sessionbeforeEach - Single thread required —
for simnet isolationsingleThread: true - Functions over arrow functions for test helpers
- Use
andcvToValue()
for Clarity-to-JS conversioncvToJSON()
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:
| Template | File | Description |
|---|---|---|
| templates/heartbeat-registry.md | Agent heartbeat with full chain context, address enumeration, liveness checks |
| templates/proof-of-existence.md | Document timestamping with SIP-018 signatures, first-write-wins, attestor index |
| templates/registry-minimal.md | Minimal registry combining snapshot + stats + events |
Execution Cost Limits
| Category | Block Limit | Read-Only Limit |
|---|---|---|
| Runtime | 5,000,000,000 | 1,000,000,000 |
| Read count | 15,000 | 30 |
| Read bytes | 100,000,000 | 100,000 |
| Write count | 15,000 | 0 |
| Write bytes | 15,000,000 | 0 |
Cost Optimization Tips
- Inline single-use values — avoid unnecessary
bindingslet - Constants over data-vars — constants are cheaper to read
- Bulk operations — single call with list beats multiple calls
- Separate params vs tuples — flat params are cheaper for function calls
- Off-chain computation — move non-essential logic to UI/indexer
Quick Reference
- Use
notstacks-block-height
(deprecated)block-height - Use
for token operations,tx-sender
only when immediate caller identity is neededcontract-caller - Use
for error propagation,try!
for guards before state changesasserts! - All public functions must return
(response ok err) - Error codes should be unique constants
- Events:
format{notification: "event-name", payload: {...}} - Check costs with
in clarinet console::get_costs
References
- friedger/clarity-ccip-026 — All 4 testing tools integrated
- kenny-stacks/stacks-starter — Testing setup reference
- aibtcdev/aibtcdev-daos — DAO patterns
- citycoins/protocol — Token and treasury patterns
- clarigen — TypeScript type generation from contracts
- secondlayer — Alternative type generator