Skip to main content

Working with Contracts

Scaffold Stacks makes it easy to create, test, and deploy Clarity smart contracts with auto-generated TypeScript bindings.

Creating contracts

Use the CLI to add new contracts to your project:
# Create a blank contract
stacksdapp add message

# Create a SIP-010 fungible token
stacksdapp add my-token --template sip010

# Create a SIP-009 NFT
stacksdapp add my-nft --template sip009

Sample contract: Message Board

Here’s a complete Clarity contract that demonstrates key concepts including sBTC payments, data storage, and read-only functions. This is based on the official Stacks Developer Quickstart.
;; Simple Message Board Contract
;; This contract allows users to read and post messages for a fee in sBTC.

;; Define contract owner
(define-constant CONTRACT_OWNER tx-sender)

;; Define error codes
(define-constant ERR_NOT_ENOUGH_SBTC (err u1004))
(define-constant ERR_NOT_CONTRACT_OWNER (err u1005))
(define-constant ERR_BLOCK_NOT_FOUND (err u1003))

;; Define a map to store messages
;; Each message has an ID, content, author, and Bitcoin block height timestamp
(define-map messages
  uint
  {
    message: (string-utf8 280),
    author: principal,
    time: uint,
  }
)

;; Counter for total messages
(define-data-var message-count uint u0)

;; Public function to add a new message for 1 satoshi of sBTC
(define-public (add-message (content (string-utf8 280)))
  (let ((id (+ (var-get message-count) u1)))
    (try! (restrict-assets? contract-caller 
      ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token "sbtc-token" u1))
      (unwrap!
        ;; Charge 1 satoshi of sBTC from the caller
        (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
          transfer u1 contract-caller current-contract none
        )
        ERR_NOT_ENOUGH_SBTC
      )
    ))
    ;; Store the message with current Bitcoin block height
    (map-set messages id {
      message: content,
      author: contract-caller,
      time: burn-block-height,
    })
    ;; Update message count
    (var-set message-count id)
    ;; Emit event for the new message
    (print {
      event: "[Stacks Dev Quickstart] New Message",
      message: content,
      id: id,
      author: contract-caller,
      time: burn-block-height,
    })
    ;; Return the message ID
    (ok id)
  )
)

;; Withdraw function for contract owner to withdraw accumulated sBTC
(define-public (withdraw-funds)
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) (err u1005))
    (let ((balance (unwrap-panic (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
        get-balance current-contract
      ))))
      (if (> balance u0)
        (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
          transfer balance current-contract CONTRACT_OWNER none
        )
        (ok false)
      )
    )
  )
)

;; Read-only function to get a message by ID
(define-read-only (get-message (id uint))
  (map-get? messages id)
)

;; Read-only function to get message author
(define-read-only (get-message-author (id uint))
  (get author (map-get? messages id))
)

Contract requirements

For contracts that depend on external contracts (like sBTC), add requirements:
cd contracts

clarinet requirements add SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-deposit
This wires up the official contracts for local testing and deployment.

Key concepts

sBTC Integration

This contract uses sBTC (Stacks’ 1:1 backed Bitcoin) for payments:
  • Requires the sBTC contract as a dependency
  • Uses restrict-assets? for post-conditions
  • Transfers sBTC using the official token contract

Data Storage

  • Maps: Store structured data (messages with metadata)
  • Data variables: Track counters and global state
  • Constants: Define immutable values

Access Control

  • CONTRACT_OWNER for privileged operations
  • asserts! for authorization checks
  • tx-sender vs contract-caller for security

Read-Only Functions

  • Query contract state without transactions
  • Use at-block for historical data
  • Return Optional types for safe lookups

Testing contracts

Write tests in contracts/tests/ using the Clarinet SDK:
	import { Cl, ClarityType } from "@stacks/transactions";
import { beforeEach, describe, expect, it } from "vitest";
import { initSimnet } from '@stacks/clarinet-sdk';

let simnet: Awaited<ReturnType<typeof initSimnet>>;
let accounts: ReturnType<Awaited<ReturnType<typeof initSimnet>>['getAccounts']>;
let deployer: string;
let address1: string;

beforeEach(async () => {
  simnet = await initSimnet();
  accounts = simnet.getAccounts();
  deployer = accounts.get("deployer")!;
  address1 = accounts.get("wallet_1")!;
});

describe("example tests", () => {
  let content = "Hello Stacks Devs!"

  it("allows user to add a new message", () => {
    let currentBurnBlockHeight = simnet.burnBlockHeight;

    let confirmation = simnet.callPublicFn(
      "message",
      "add-message",
      [Cl.stringUtf8(content)],
      address1
    )

    const messageCount = simnet.getDataVar("message", "message-count");
    
    expect(confirmation.result.type).toBe(ClarityType.ResponseOk);
    expect(confirmation.result).toEqual(Cl.ok(messageCount));
    expect(confirmation.events[1].data.value).toEqual(Cl.tuple({
      author: Cl.standardPrincipal(address1),
      event: Cl.stringAscii("[Stacks Dev Quickstart] New Message"),
      id: messageCount,
      message: Cl.stringUtf8(content),
      time: Cl.uint(currentBurnBlockHeight),
    }));
  });

  it("allows contract owner to withdraw funds", () => {
    simnet.callPublicFn(
      "message",
      "add-message",
      [Cl.stringUtf8(content)],
      address1
    )
    
    simnet.mineEmptyBurnBlocks(2);

    let confirmation = simnet.callPublicFn(
      "message",
      "withdraw-funds",
      [],
      deployer
    )
    
    expect(confirmation.result).toEqual(Cl.ok(Cl.bool(true)));
    expect(confirmation.events[0].event).toBe("ft_transfer_event")
    expect(confirmation.events[0].data).toMatchObject({
      amount: '1',
      asset_identifier: 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token',
      recipient: deployer,
      sender: `${deployer}.message`,
    })
  })
});

	
Run tests with:
stacksdapp test