TABLE OF CONTENTS
Introduction
Prerequisite
Installing Clarinet
On macOS
On Windows
Creating a new Clarinet project
- Adding a new contract
Creating and implementing SIP-10 traits
SIP-010 traits?
Creating and Implementing SIP-010 trait functions
Outlining the different sections of the contract
SIP-010 functions
Public functions
Read-only functions
Mint function
Manually testing
Mint token
Transfer token
Check Balance
Conclusion
References
Introduction
By the time you are done reading this article, you will have the knowledge necessary to create the safest fungible token smart contract. Clarity is a language for writing smart contracts that run on a blockchain network. Clarity is designed to create safe, predictable, secure, and readable smart contracts, and it is built to handle the development of technologies handling high-stakes transactions.
Prerequisite
Before delving further, there are a couple of things you should know and have:
A firm understanding of the programming language Clarity
Understand how smart contracts work
Visual Studio Code
And this awesome playlist I listened to when writing this (This prerequisite can be skipped if you have a better playlist you'd rather listen to)
Installing Clarinet
What is the clarinet?
Clarinet is a command-line tool used by Clarity to manage the processing smart contract processing, development, testing, and deployment. Clarinet also uses a testing interface and Read-Eval-Print loop (REPL), which allows you to test the behavior of the smart contract before pushing it to a local devnet or testnet.
Clarity was designed for the stack blockchain to optimize for predictability and security, which enables developers to know exactly how their smart contracts will work and encode the contract logic that handles day-to-day transactions securely.
Installing on macOS
To install clarinet on macOS, the package manager Homebrew is required. Using thebrew
command, it's easy to add powerful functionality to your Mac, but first, it needs to be installed.
First, launch your terminal and make sure you have XCode installed, as Homebrew will need it when setting up.
xcode-select --install
Once the installation of XCode has been completed, we can proceed to install Clarinet by running the command:
brew install clarinet
Installing on Windows
Installing Clarinet on Windows is more straightforward. This is done using the MSI installer, which can be downloaded from its release page.
Clarinet can also be found on Winget, a package manager that is now included in recent versions of Windows.
Winget install clarinet
The installation can be verified by running the command clarinet --version
on the terminal.
Creating a new Clarinet project
To get started, we'll be creating a folder that will contain all of our projects. We can call ours project-x.
Open the folder using Visual Studio Code; once the folder has been created, run the code:
clarinet new fungible-token
Created directory fungible-token
Created directory fungible-token/contracts
Created directory fungible-token/settings
Created directory fungible-token/tests
Created file fungible-token/Clarinet.toml
Created file fungible-token/settings/Mainnet.toml
Created file fungible-token/settings/Testnet.toml
Created file fungible-token/settings/Devnet.toml
Created directory fungible-token/.vscode
Created file fungible-token/.vscode/settings.json
Created file fungible-token/.vscode/tasks.json
Created file fungible-token/.gitignore
Clarinet is going to create some directories and files. The fungible-token
directory will be where we will work from. You can open the folder in Visual Studio Code or navigate into it using the CLI with the command cd fungible-token
.
Adding a new contract
Inside the fungible-token folder, to create a new Clarity contract, we will run the command:
clarinet contract new simple-ft-token
Clarinet will create a template clarity and test file and add the new contract to the configuration file.
Created file contracts/simple-ft-token.clar
Created file tests/simple-ft-token_test.ts
Updated Clarinet.toml with contract simple-ft-token
Creating and implementing SIP-10 traits
SIP-10 traits?
SIP-10 traits are a defined list of signatures (i.e., the name, parameters (inputs), and output types) that every fungible token should have. These traits are built into Clarity and are implicitly added to fungible tokens when they are created.
The fungible trait has seven functions:
Transfer
(transfer ((amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) (response bool uint))
This function handles the transfer of tokens from the sender to the recipient. It takes five parameters: amount, two principals (the addresses of the sender and recipient), an optional memo parameter (why this parameter is needed, I have no idea), and a response parameter that returns a boolean if the transfer succeeds and a uint (a defined constant) if unsuccessful.
Name
(get-name () (response (string-ascii 32) uint))
This function returns the name of the contract and is defined as read-only.
Symbol
(get-symbol () (response (string-ascii 32) uint))
This function returns a symbol that represents a shorter version of the token. Some examples are: "BTC", "ETH", "STX", etc. This function is also defined as read-only.
Decimals
(get-decimals () (response uint uint))
This function returns the number of decimal places a token has. It is ideal to have the decimal places defined, as this allows the token to accurately render value. Bitcoin has 8 decimal places, Stacks has 6 decimal places, and the US dollar has 2 decimal places; basically, having a defined decimal provides more precision. This is also defined as read-only.
Balance of
(get-balance (principal) (response uint uint))
This function returns the balance of a principal (which can also be referred to as an "address" or "account"). This function is defined as read-only.
Total supply
(get-total-supply () (response uint uint))
This function returns the total supply of the token, i.e., the current amount of token in circulation. This function is defined as read-only.
Token URI
(get-token-uri () (response (optional (string-utf8 256)) uint))
This function returns the token's metadata, which is an optional string that links to a uniform resource identifier (URI) stored off-chain. An example of a metadata file is outlined below:
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this token represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this token represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
}
}
}
The function returns none if the token has no URI linked to it.
Implementing the SIP-010 trait
To implement the SIP-010 trait functions, let's create another contract in our fungible-token directory and implement the functions in our token contract. First, we run the command:
clarinet contract new sip-010-trait
Created file contracts/sip-010-trait.clar
Created file tests/sip-010-trait_test.ts
Updated Clarinet.toml with contract sip-010-trait
Now that we've created our contract, let's add the SIP-010 trait functions:
(define-trait ft-trait
(
;; Transfer from principal to principal
(transfer (uint principal principal (optional (buff 34))) (response bool uint))
;; Human-readable name of the token
(get-name () (response (string-ascii 32) uint))
;; Human-readable symbol
(get-symbol () (response (string-ascii 32) uint))
;; Number of decimals used to represent the token
(get-decimals () (response uint uint))
;; Balance
(get-balance (principal) (response uint uint))
;; Current total supply
(get-total-supply () (response uint uint))
;; Optional URI for metadata
(get-token-uri () (response (optional (string-ascii 356)) uint))
)
)
With the trait defined, we can now head over to our simple-ft-token contract and implement it by using the impl-trait
function to pass the defined traits.
(impl-trait .sip-010.ft-trait)
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Constants & Variables ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-fungible-token hash-token)
(define-constant contract-owner tx-sender)
(define-constant ERR-OWNER-ONLY u100)
(define-constant err-not-token-owner u101)
Once the contract has been created, we start mapping out the different sections:
The first line implicitly adds the SIP-010 fungible-token trait to the contract
The section below outlines the constants and variables:
(define-fungible-token hash-token)
: defines the name of our token,hash-token
(define-constant contract-owner tx-sender)
: defines a constantcontract-owner
and sets the value totx-sender.
(define-constant err-not-token-owner u101)
defines a constanterr-not-token-owner
and assigns it to an unsigned integer. This message is returned when a user tries to access tokens they do not own.
SIP-010 functions
In this section, we are going to implement the SIP-010 trait:
Transfer
;;;;;;;;;;;;;;
;; SIP-010 ;;
;;;;;;;;;;;;;;
;; Transfer
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
(begin
(asserts! (is-eq sender tx-sender) (err ERR-NOT-AUTHORIZED))
(unwrap! (ft-transfer? LP-token amount sender recipient) (err err-not-token-owner))
(match memo to-print (print to-print) 0x)
(ok true)
)
)
The transfer function checks to ensure that the sender
is equal to tx-sender
to ensure that principals
cannot have access to tokens they do not own. Theunwrap!
function is also used here to return a response and print the memo
if it is not none
.
get-name
(define-read-only (get-name)
(ok "hash-token")
)
This is a read-only function that returns the name of the token.
get-symbol
(define-read-only (get-symbol)
(ok "HT")
)
This is a read-only function that returns the token's symbol.
get-decimals
(define-read-only (get-decimals)
(ok u6)
)
This is a read-only function that returns how many decimals a token has. This function helps determine the exact amount of tokens a user owns or wants to send.
get-balance
(define-read-only (get-balance (owner principal))
(ok (ft-get-balance hash-token account))
)
This is a read-only function that returns the balance of a principal
. Clarity has a built-in function that returns the balance of a principal
.
get-total-supply
(define-read-only (get-total-supply)
(ok (ft-get-supply hash-token))
)
This function returns the total supply of our hash-token
. Clarity also has a function that handles this and retrieves the total number of tokens supplied.
(define-read-only (get-token-uri)
(ok none)
)
This function returns a link that points to a metadata file for the token. Our fungible token has no metadata file, so we just return none
.
mint
(define-public (mint (amount uint) (recipient principal))
(ft-mint? hash-token amount recipient)
)
This is a public function that can only be called by the deployer of the contract to mint (create) new tokens that will be distributed to different principals (addresses).
Manual testing
In this section, we are going to test the contract manually to make sure it has the functionalities of a working, fungible token contract. We will mint some tokens, transfer tokens from one address to another, and then check the balance of the addresses to make sure the tokens were sent and received.
To begin testing our contract manually, we will run the command clarinet console
in our terminal to enter the console section.
Mint token
>> (contract-call? .simple-ft-token mint u2000 tx-sender)
Events emitted
{"type":"ft_mint_event","ft_mint_event":{"asset_identifier":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.simple-ft-token::hash-token","recipient":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM","amount":"2000"}}
(ok true)
FT (fungible-token) mint event returns an OK response; we have successfully minted 2000 hash-tokens to tx-sender (contract-owner). Now we can send tokens to an address in our contract.
Transfer tokens
>> (contract-call? .simple-ft-token transfer u400 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 none)
Events emitted
{"type":"ft_transfer_event","ft_transfer_event":{"asset_identifier":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.simple-ft-token::hash-token","sender":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM","recipient":"ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5","amount":"400"}}
(ok true)
The first address in our clarinet console is the contract's address that holds the newly minted token. We use none
as a value for our memo
as we will not be making use of it.
Check Balance
Now that we have sent some tokens to another principal, let's check our balance to make sure the exact amount got sent and the receiving principal (address) received the token.
To check the balance, we run the command ::get_assets_maps
in the clarinet console.
>> ::get_assets_maps
+-------------------------------------------+-----------------------------+-----------------+
| Address | .simple-ft-token.hash-token | uSTX |
+-------------------------------------------+-----------------------------+-----------------+
| ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM | 1600 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 | 400 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
From the result above, we can see that we have successfully transferred 400 hash tokens from our contract to another address. Let's try sending from the address that just received some tokens to another address.
>> ::set_tx_sender ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
tx-sender switched to ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
To do this, we have to first set the tx-sender
to the address sending the token. This is done by running the command ::set_tx_sender
followed by the address shown above.
Now that we have changed the sending address to a different one from which the contract was deployed, we have to specify the full contract principal. The full contract principal can be gotten from the contract identifier section that comes up when we run the clarinet console command.
+-----------------------------------------------------------+-----------------------------------+
| Contract identifier | Public functions
|
+-----------------------------------------------------------+-----------------------------------+
| ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.simple-ft-token | (get-balance (account principal)) |
| | (get-decimals)
|
| | (get-name)
|
| | (get-symbol)
|
| | (get-token-uri)
|
| | (get-total-supply)
|
| | (mint
|
| | (amount uint)
Now we are going to send some tokens from our second address to the third address and check our balance to make sure everything is in order.
(contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.simple-ft-token transfer u100 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG none)
Events emitted
{"type":"ft_transfer_event","ft_transfer_event":{"asset_identifier":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.simple-ft-token::hash-token","sender":"ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5","recipient":"ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG","amount":"100"}}
(ok true)
Now we check our balance by running the command ::get_assets_maps
>> ::get_assets_maps
+-------------------------------------------+-----------------------------+-----------------+
| Address | .simple-ft-token.hash-token | uSTX |
+-------------------------------------------+-----------------------------+-----------------+
| ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM | 1600 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 | 300 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG | 100 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ | 0 | 100000000000000 |
+-------------------------------------------+-----------------------------+-----------------+
| STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 | 0 | 100000000000000 |
+-------------------------------------------+--------------------------------+---------------
Conclusion
Fungible tokens play a significant role in the world of blockchain and cryptocurrency, so they must be created with the safest means possible as they serve as digital currencies, stablecoins, and traditional asset tokenization. Other advanced properties can be added to fungible tokens to give them more functionalities, but what's essential is, that a fungible token should be secure, simplify transactions, and enhance efficiency.