Building Your First SIP-010 Token on Stacks

FlamesFlames
9 min read

Introduction

Welcome to the first part of our STX.CITY Mini tutorial series! In this tutorial, you'll learn how to create a professional SIP-010 compliant token on the Stacks blockchain from scratch. By the end of this guide, you'll have a fully functional token contract that can be minted, transferred, and traded.

What You'll Learn

  • Understanding SIP-010 token standard

  • Setting up a Clarinet development environment

  • Writing and deploying token contracts

  • Implementing ownership and metadata management

  • Testing your token with Clarinet console

Prerequisites

  • Basic understanding of blockchain concepts

  • Familiarity with command line interface

  • Text editor or IDE

Setting Up Your Development Environment

First, let's set up our Clarinet project for Stacks development.

1. Install Clarinet

# Install Clarinet (Stacks development framework)
# Follow installation instructions at https://docs.hiro.so/clarinet

2. Create a New Project

# Create new Clarinet project
clarinet new stx-city-mini
cd stx-city-mini

3. Add Required Dependencies

Since we're building on testnet, we need to add the SIP-010 trait dependency:

# Add SIP-010 trait requirement for testnet
clarinet requirements add STF0V8KWBS70F0WDKTMY65B3G591NN52PR4Z71Y3.sip-010-trait-ft-standard

What this does: This command adds the SIP-010 trait interface to our project, which defines the standard functions that all fungible tokens must implement on Stacks.

Understanding SIP-010 Token Standard

SIP-010 is the Stacks Improvement Proposal that defines the standard interface for fungible tokens. It's similar to ERC-20 on Ethereum and ensures interoperability between different tokens and applications.

Required Functions

Every SIP-010 token must implement these functions:

  • transfer - Move tokens between addresses

  • get-name - Return token name

  • get-symbol - Return token symbol

  • get-decimals - Return decimal places

  • get-total-supply - Return total token supply

  • get-balance - Return balance for an address

  • get-token-uri - Return metadata URI

Building Our Token Contract

Let's create our token contract step by step. Create a new contract which we would name token here.

clarinet contract new token

1. Contract Header and Constants

;; @title Bonding Curve Token for STX.CITY Mini Version
;; @version 1.0
;; @summary A SIP-010 compliant fungible token for memecoin launches
;; @description This contract implements a fungible token that can be traded on bonding curves

;; Error constants
(define-constant ERR-UNAUTHORIZED u401)
(define-constant ERR-NOT-OWNER u402)
(define-constant ERR-INVALID-PARAMETERS u403)
(define-constant ERR-NOT-ENOUGH-FUND u101)

What this does:

  • The header provides metadata about our contract

  • Error constants define specific error codes for different failure scenarios

  • Using constants makes our code more readable and maintainable

2. SIP-010 Implementation and Token Definition

;; Implement the SIP-010 trait
(impl-trait 'STF0V8KWBS70F0WDKTMY65B3G591NN52PR4Z71Y3.sip-010-trait-ft-standard.sip-010-trait)

;; Token constants
(define-constant MAXSUPPLY u100000000000000) ;; 100M tokens with 6 decimals

;; Define the fungible token
(define-fungible-token MINI-TOKEN MAXSUPPLY)

;; Data variables
(define-data-var contract-owner principal tx-sender)
(define-data-var token-uri (optional (string-utf8 256)) none)

What this does:

  • impl-trait tells Clarity we're implementing the SIP-010 interface

  • MAXSUPPLY sets our token limit to 100 million (with 6 decimal places)

  • define-fungible-token creates our actual token called MINI-TOKEN

  • contract-owner tracks who owns the contract

  • token-uri stores optional metadata URL

3. Core SIP-010 Functions

;; SIP-010 Functions
(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34))))
    (begin
        (asserts! (is-eq from tx-sender) (err ERR-UNAUTHORIZED))
        (try! (ft-transfer? MINI-TOKEN amount from to))
        (ok true)
    )
)

(define-read-only (get-name)
    (ok "Mini Token")
)

(define-read-only (get-symbol)
    (ok "MINI")
)

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

(define-read-only (get-total-supply)
    (ok (ft-get-supply MINI-TOKEN))
)

(define-read-only (get-balance (owner principal))
    (ok (ft-get-balance MINI-TOKEN owner))
)

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

What this does:

  • transfer moves tokens between addresses with authorization check

  • get-name, get-symbol return static token information

  • get-decimals returns 6 (meaning 1 token = 1,000,000 micro-tokens)

  • get-total-supply and get-balance query current token state

  • get-token-uri returns metadata URL for token information

4. Owner Management Functions

;; Owner-only functions
(define-public (set-token-uri (value (string-utf8 256)))
    (begin
        (asserts! (is-eq tx-sender (var-get contract-owner)) (err ERR-UNAUTHORIZED))
        (var-set token-uri (some value))
        (ok (print {
                notification: "token-metadata-update",
                payload: {
                contract-id: (as-contract tx-sender),
                token-class: "ft"
                }
            })
        )
    )
)

(define-public (transfer-ownership (new-owner principal))
    (begin
        (asserts! (is-eq tx-sender (var-get contract-owner)) (err ERR-NOT-OWNER))
        (var-set contract-owner new-owner)
        (ok "Ownership transferred successfully")
    )
)
/cod

What this does:

  • set-token-uri allows the owner to update token metadata

  • transfer-ownership enables changing contract ownership

  • Both functions use asserts! to ensure only the owner can call them

  • The metadata update emits a notification event for wallets/applications

5. Utility Functions

;; Utility Functions
(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) })))
    (fold check-err (map send-token recipients) (ok true))
)

(define-private (check-err (result (response bool uint)) (prior (response bool uint)))
    (match prior ok-value result err-value (err err-value))
)

(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) }))
    (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient))
)

(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34))))
    (let ((transferOk (try! (transfer amount tx-sender to memo))))
        (ok transferOk)
    )
)

What this does:

  • send-many enables batch token transfers (up to 200 recipients)

  • Uses functional programming with fold and map for efficient processing

  • Includes error handling to stop if any transfer fails

  • Very useful for airdrops or batch payments

6. Mint Function and Initialization

;; Mint function (called during contract initialization)
(define-public (mint (amount uint) (recipient principal))
    (begin
        (asserts! (is-eq tx-sender (var-get contract-owner)) (err ERR-UNAUTHORIZED))
        (try! (ft-mint? MINI-TOKEN amount recipient))
        (ok true)
    )
)

;; Initialize token supply on deployment
(begin
    (try! (ft-mint? MINI-TOKEN MAXSUPPLY tx-sender))
)

What this does:

  • mint function allows owner to create new tokens (up to max supply)

  • The (begin ...) block at the end runs when contract is deployed

  • Automatically mints all 100M tokens to the deployer

  • This ensures the token has supply immediately after deployment

Testing Your Token Contract

Now let's test our token to make sure everything works correctly.

1. Check Contract Compilation

# Verify the contract compiles without errors
clarinet check

You should see output indicating successful compilation.

2. Test with Clarinet Console

# Start interactive Clarity console
clarinet console

3. Basic Function Testing

;; Test getting token information
>> (contract-call? .token get-name)
(ok "Mini Token")

>> (contract-call? .token get-symbol)  
(ok "MINI")

>> (contract-call? .token get-decimals)
(ok u6)

>> (contract-call? .token get-total-supply)
(ok u100000000000000)

What this shows: All our read-only functions return the expected values.

4. Test Token Balance

;; Check deployer's balance (should have all tokens)
>> (contract-call? .token get-balance tx-sender)
(ok u100000000000000)

What this shows: The deployer received all 100M tokens during initialization.

5. Test Token Transfer

;; Switch to a different address for testing
>> ::set_tx_sender ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5

;; Try to transfer tokens from deployer to current address
>> (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token transfer u1000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 none)

This should fail with (err u401) because we're not authorized to transfer from the deployer's account.

6. Test Send-Many Function

;; Switch back to deployer
>> ::set_tx_sender ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM

;; Test batch transfer
>> (contract-call? .token send-many (list 
    { to: 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5, amount: u1000000, memo: none }
    { to: 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG, amount: u2000000, memo: none }
))

This should succeed and transfer tokens to multiple recipients.

Understanding the Results

If all tests pass, you've successfully created a fully functional SIP-010 token! Here's what you've accomplished:

  1. Standards Compliance: Your token implements all required SIP-010 functions

  2. Security: Authorization checks prevent unauthorized transfers

  3. Flexibility: Owner functions allow metadata updates and ownership transfer

  4. Utility: Batch transfer function enables efficient token distribution

  5. Initialization: Automatic token minting on deployment

Key Learning Points

Token Economics

  • Supply Management: Fixed 100M token supply with 6 decimals

  • Distribution: All tokens start with the deployer

  • Precision: 6 decimals means 1 token = 1,000,000 micro-tokens

Security Features

  • Authorization: Only token holders can transfer their tokens

  • Ownership: Contract owner has special privileges

  • Error Handling: Clear error codes for different failure scenarios

Clarity Programming Concepts

  • Traits: Using impl-trait for interface compliance

  • Assertions: Using asserts! for validation

  • Functional Programming: Using map, fold, and match

  • State Management: Using data variables for mutable state

Next Steps

Congratulations! You've built a complete SIP-010 token contract. In Part 2 of this series, we'll create a bonding curve DEX that allows users to trade your token with automatic pricing and liquidity provision.

What's Coming Next

  • Bonding curve mathematics

  • Automated market maker implementation

  • Fee distribution mechanisms

  • Contract interaction patterns

Complete Code

Here's the complete token contract for reference:

;; @title Bonding Curve Token for STX.CITY Mini Version
;; @version 1.0
;; @summary A SIP-010 compliant fungible token for memecoin launches
;; @description This contract implements a fungible token that can be traded on bonding curves

;; Error constants
(define-constant ERR-UNAUTHORIZED u401)
(define-constant ERR-NOT-OWNER u402)
(define-constant ERR-INVALID-PARAMETERS u403)
(define-constant ERR-NOT-ENOUGH-FUND u101)

;; Implement the SIP-010 trait
(impl-trait 'STF0V8KWBS70F0WDKTMY65B3G591NN52PR4Z71Y3.sip-010-trait-ft-standard.sip-010-trait)

;; Token constants
(define-constant MAXSUPPLY u100000000000000) ;; 100M tokens with 6 decimals

;; Define the fungible token
(define-fungible-token MINI-TOKEN MAXSUPPLY)

;; Data variables
(define-data-var contract-owner principal tx-sender)
(define-data-var token-uri (optional (string-utf8 256)) none)

;; SIP-010 Functions
(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34))))
    (begin
        (asserts! (is-eq from tx-sender) (err ERR-UNAUTHORIZED))
        (try! (ft-transfer? MINI-TOKEN amount from to))
        (ok true)
    )
)

(define-read-only (get-name)
    (ok "Mini Token")
)

(define-read-only (get-symbol)
    (ok "MINI")
)

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

(define-read-only (get-total-supply)
    (ok (ft-get-supply MINI-TOKEN))
)

(define-read-only (get-balance (owner principal))
    (ok (ft-get-balance MINI-TOKEN owner))
)

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

;; Owner-only functions
(define-public (set-token-uri (value (string-utf8 256)))
    (begin
        (asserts! (is-eq tx-sender (var-get contract-owner)) (err ERR-UNAUTHORIZED))
        (var-set token-uri (some value))
        (ok (print {
                notification: "token-metadata-update",
                payload: {
                contract-id: (as-contract tx-sender),
                token-class: "ft"
                }
            })
        )
    )
)

(define-public (transfer-ownership (new-owner principal))
    (begin
        (asserts! (is-eq tx-sender (var-get contract-owner)) (err ERR-NOT-OWNER))
        (var-set contract-owner new-owner)
        (ok "Ownership transferred successfully")
    )
)

;; Utility Functions
(define-public (send-many (recipients (list 200 { to: principal, amount: uint, memo: (optional (buff 34)) })))
    (fold check-err (map send-token recipients) (ok true))
)

(define-private (check-err (result (response bool uint)) (prior (response bool uint)))
    (match prior ok-value result err-value (err err-value))
)

(define-private (send-token (recipient { to: principal, amount: uint, memo: (optional (buff 34)) }))
    (send-token-with-memo (get amount recipient) (get to recipient) (get memo recipient))
)

(define-private (send-token-with-memo (amount uint) (to principal) (memo (optional (buff 34))))
    (let ((transferOk (try! (transfer amount tx-sender to memo))))
        (ok transferOk)
    )
)

;; Mint function (called during contract initialization)
(define-public (mint (amount uint) (recipient principal))
    (begin
        (asserts! (is-eq tx-sender (var-get contract-owner)) (err ERR-UNAUTHORIZED))
        (try! (ft-mint? MINI-TOKEN amount recipient))
        (ok true)
    )
)

;; Initialize token supply on deployment
(begin
    (try! (ft-mint? MINI-TOKEN MAXSUPPLY tx-sender))
)

This token contract provides a solid foundation for building decentralized applications on Stacks. In the next tutorial, we'll explore how to create a decentralized exchange that can trade this token using bonding curve mechanics!

0
Subscribe to my newsletter

Read articles from Flames directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Flames
Flames