Skip to main content
Learn everything about fungible tokens (FTs) on NEAR by building a fully-featured FT smart contract that implements all standard extensions.

Overview

This comprehensive tutorial series guides you from using a pre-deployed FT contract to implementing every aspect of the NEP-141 fungible token standard and its extensions.

FT Tutorial Series

Complete step-by-step fungible token tutorial

What you’ll learn

NEP-141 Core

The foundational FT standard for creating and transferring tokens

NEP-148 Metadata

Add token name, symbol, decimals, and icon

NEP-145 Storage

Implement storage management to prevent state bloat

Token Economics

Design tokenomics, supply, and distribution

Marketplace Integration

Enable buying NFTs with your fungible token

FT Events

Emit events for transfers and mints

Prerequisites

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install cargo-near
cargo install cargo-near

# Install NEAR CLI
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/near/near-cli-rs/releases/latest/download/near-cli-rs-installer.sh \
  | sh

# Create testnet account
near account create-account sponsor-by-faucet-service your-name.testnet \
  autogenerate-new-keypair save-to-keychain network-config testnet create
New to Rust? Start with the smart contract quickstart to learn the basics.

Tutorial steps

Progress through these chapters to master fungible tokens:
1

Pre-deployed contract

Interact with an existing FT contract. Receive tokens without writing any code!Begin tutorial →
2

Contract architecture

Understand the FT contract structure and compile the skeleton code.View chapter →
3

Defining your token

Customize your token with name, symbol, decimals, and total supply.View chapter →
4

Circulating supply

Create an initial token supply and see your tokens in wallets.View chapter →
5

Registering accounts

Implement storage management to prevent account draining attacks.View chapter →
6

Transferring tokens

Implement the core transfer functionality.View chapter →
7

Marketplace integration

Use your FT to buy NFTs on a marketplace.View chapter →

Quick start example

Here’s a minimal fungible token contract:
use near_sdk::{
    near, AccountId, PanicOnDefault,
    collections::LookupMap,
    json_types::U128,
};

#[near(serializers = [json, borsh])]
pub struct FungibleTokenMetadata {
    pub spec: String,
    pub name: String,
    pub symbol: String,
    pub decimals: u8,
}

#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct FungibleToken {
    pub owner_id: AccountId,
    pub total_supply: u128,
    pub balances: LookupMap<AccountId, u128>,
    pub metadata: FungibleTokenMetadata,
}

#[near]
impl FungibleToken {
    #[init]
    pub fn new(
        owner_id: AccountId,
        total_supply: U128,
        metadata: FungibleTokenMetadata,
    ) -> Self {
        let mut ft = Self {
            owner_id: owner_id.clone(),
            total_supply: total_supply.0,
            balances: LookupMap::new(b"b"),
            metadata,
        };
        
        // Give total supply to owner
        ft.balances.insert(&owner_id, &total_supply.0);
        
        ft
    }
    
    pub fn ft_transfer(
        &mut self,
        receiver_id: AccountId,
        amount: U128,
    ) {
        let sender_id = env::predecessor_account_id();
        let amount = amount.0;
        
        // Get balances
        let sender_balance = self.balances.get(&sender_id)
            .expect("Account not registered");
        let receiver_balance = self.balances.get(&receiver_id)
            .expect("Receiver not registered");
        
        // Check sufficient balance
        require!(
            sender_balance >= amount,
            "Insufficient balance"
        );
        
        // Update balances
        self.balances.insert(&sender_id, &(sender_balance - amount));
        self.balances.insert(&receiver_id, &(receiver_balance + amount));
    }
    
    pub fn ft_balance_of(&self, account_id: AccountId) -> U128 {
        self.balances.get(&account_id).unwrap_or(0).into()
    }
}

FT standards (NEPs)

The foundation for all fungible tokens on NEAR. Defines:
  • Token transfers
  • Balance queries
  • Total supply tracking
Read the standard
Standardizes token information:
  • Token name (e.g., “US Dollar Coin”)
  • Symbol (e.g., “USDC”)
  • Decimals (e.g., 6)
  • Icon URL
Read the standard
Prevents state bloat attacks:
  • Accounts must pay for their storage
  • Registration before receiving tokens
  • Storage deposit refunds
Read the standard
Emit events for indexers:
  • ft_mint events
  • ft_transfer events
  • ft_burn events
Read the standard

Storage management

A critical security feature:
#[near]
impl FungibleToken {
    #[payable]
    pub fn storage_deposit(
        &mut self,
        account_id: Option<AccountId>,
    ) -> StorageBalance {
        let account = account_id.unwrap_or_else(env::predecessor_account_id);
        let deposit = env::attached_deposit();
        
        // Check if already registered
        if self.balances.contains_key(&account) {
            // Refund deposit
            if deposit > 0 {
                Promise::new(env::predecessor_account_id())
                    .transfer(deposit);
            }
        } else {
            // Register account
            let min_balance = self.storage_balance_bounds().min;
            require!(
                deposit >= min_balance,
                format!("Deposit must be at least {}", min_balance)
            );
            
            self.balances.insert(&account, &0);
            
            // Refund excess
            let refund = deposit - min_balance;
            if refund > 0 {
                Promise::new(env::predecessor_account_id())
                    .transfer(refund);
            }
        }
        
        self.storage_balance_of(account).unwrap()
    }
}

Testing your FT contract

Comprehensive testing example:
#[tokio::test]
async fn test_ft_transfer() -> Result<()> {
    let worker = near_workspaces::sandbox().await?;
    let contract = worker.dev_deploy(include_bytes!("../target/near/ft.wasm")).await?;
    
    // Initialize
    contract.call("new")
        .args_json(serde_json::json!({
            "owner_id": contract.id(),
            "total_supply": "1000000",
            "metadata": {
                "spec": "ft-1.0.0",
                "name": "Test Token",
                "symbol": "TEST",
                "decimals": 18
            }
        }))
        .transact()
        .await?;
    
    // Create test account
    let alice = worker.dev_create_account().await?;
    
    // Register alice
    alice.call(contract.id(), "storage_deposit")
        .args_json(serde_json::json!({"account_id": alice.id()}))
        .deposit(near_sdk::NearToken::from_millinear(10))
        .transact()
        .await?;
    
    // Transfer tokens
    contract.call("ft_transfer")
        .args_json(serde_json::json!({
            "receiver_id": alice.id(),
            "amount": "1000"
        }))
        .transact()
        .await?;
    
    // Verify balance
    let balance: U128 = contract
        .view("ft_balance_of")
        .args_json(serde_json::json!({"account_id": alice.id()}))
        .await?
        .json()?;
    
    assert_eq!(balance.0, 1000);
    
    Ok(())
}

Common use cases

Stablecoins

Create tokens pegged to fiat currencies

Governance tokens

Power DAO voting and decision-making

Reward points

Loyalty programs and user incentives

Gaming currency

In-game economies and virtual goods

Wrapped tokens

Bridge tokens from other chains

DeFi tokens

Build lending, DEX, and yield protocols

Token economics

Set total supply at initialization. No minting or burning:
#[init]
pub fn new(total_supply: U128) -> Self {
    // All tokens created at initialization
}
Allow authorized accounts to mint new tokens:
pub fn mint(&mut self, account_id: AccountId, amount: U128) {
    require!(env::predecessor_account_id() == self.owner_id);
    let balance = self.balances.get(&account_id).unwrap_or(0);
    self.balances.insert(&account_id, &(balance + amount.0));
    self.total_supply += amount.0;
}
Allow token holders to burn (destroy) their tokens:
pub fn burn(&mut self, amount: U128) {
    let account_id = env::predecessor_account_id();
    let balance = self.balances.get(&account_id).unwrap();
    require!(balance >= amount.0);
    self.balances.insert(&account_id, &(balance - amount.0));
    self.total_supply -= amount.0;
}

Marketplace integration

Use your FT to buy NFTs:
// In your FT contract
pub fn ft_transfer_call(
    &mut self,
    receiver_id: AccountId,
    amount: U128,
    msg: String,
) -> Promise {
    // Transfer tokens
    self.ft_transfer(receiver_id.clone(), amount);
    
    // Call receiver's ft_on_transfer
    ext_receiver::ext(receiver_id)
        .ft_on_transfer(
            env::predecessor_account_id(),
            amount,
            msg,
        )
        .then(
            Self::ext(env::current_account_id())
                .ft_resolve_transfer(/* ... */)
        )
}

Next steps

Start the tutorial

Begin building your FT contract

FT primitives

Learn about using existing FT contracts

NFT tutorial

Build non-fungible token contracts

DEX primitives

Create token swapping functionality

Build docs developers (and LLMs) love