Building Your First SIP-010 Token on Stacks

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 addressesget-name
- Return token nameget-symbol
- Return token symbolget-decimals
- Return decimal placesget-total-supply
- Return total token supplyget-balance
- Return balance for an addressget-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 interfaceMAXSUPPLY
sets our token limit to 100 million (with 6 decimal places)define-fungible-token
creates our actual token called MINI-TOKENcontract-owner
tracks who owns the contracttoken-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 checkget-name
,get-symbol
return static token informationget-decimals
returns 6 (meaning 1 token = 1,000,000 micro-tokens)get-total-supply
andget-balance
query current token stateget-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 metadatatransfer-ownership
enables changing contract ownershipBoth functions use
asserts!
to ensure only the owner can call themThe 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
andmap
for efficient processingIncludes 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 deployedAutomatically 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:
Standards Compliance: Your token implements all required SIP-010 functions
Security: Authorization checks prevent unauthorized transfers
Flexibility: Owner functions allow metadata updates and ownership transfer
Utility: Batch transfer function enables efficient token distribution
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 complianceAssertions: Using
asserts!
for validationFunctional Programming: Using
map
,fold
, andmatch
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!
Subscribe to my newsletter
Read articles from Flames directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
