Building a Simple Token with Rust: A Complete Guide
const x = () =>
<div className="...">
npm install
git commit -m
console.log()
Back to blog
rustsolanablockchaincryptocurrencytokenprogramming

Building a Simple Token with Rust: A Complete Guide

Learn how to create a basic cryptocurrency token using Rust and Solana. Step-by-step guide from setup to deployment with practical examples.

5 min read
0 views views

Building a Simple Token with Rust: A Complete Guide

Rust is becoming increasingly popular in blockchain development due to its memory safety, performance, and growing ecosystem. In this guide, we'll build a simple token using Rust on the Solana blockchain.

Why Rust for Blockchain Development?

Rust offers several advantages for blockchain development:

  • Memory Safety: Prevents common bugs like buffer overflows
  • Performance: Compiles to efficient machine code
  • Concurrency: Excellent support for parallel processing
  • Growing Ecosystem: Strong Web3 library support
  • Cross-platform: Works on multiple operating systems

Prerequisites

Before we start, make sure you have:

  • Rust installed (latest stable version)
  • Solana CLI tools
  • Basic understanding of Rust syntax
  • Familiarity with blockchain concepts

Setting Up the Project

1. Install Rust

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

# Add to PATH
source ~/.cargo/env

# Verify installation
rustc --version

2. Install Solana CLI

bash
# Install Solana CLI
sh -c "$(curl -sSfL https://release.solana.com/v1.17.0/install)"

# Add to PATH
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"

# Verify installation
solana --version

3. Create New Project

bash
# Create new Rust project
cargo new simple_token --lib
cd simple_token

# Add Solana dependencies
cargo add solana_program
cargo add borsh
cargo add thiserror

Building the Token Program

1. Basic Token Structure

Let's start with a simple token that can mint, transfer, and burn tokens:

rust
// src/lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    pubkey::Pubkey,
};

// Define the program's entry point
entrypoint!(process_instruction);

// Token instruction enum
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum TokenInstruction {
    /// Initialize a new token
    /// Accounts expected:
    /// 0. [signer] The account that will own the token
    /// 1. [writable] The token account to initialize
    Initialize {
        name: String,
        symbol: String,
        decimals: u8,
        initial_supply: u64,
    },
    /// Transfer tokens from one account to another
    /// Accounts expected:
    /// 0. [signer] The account transferring tokens
    /// 1. [writable] The source token account
    /// 2. [writable] The destination token account
    Transfer { amount: u64 },
    /// Mint new tokens to an account
    /// Accounts expected:
    /// 0. [signer] The mint authority
    /// 1. [writable] The token account to mint to
    /// 2. [writable] The mint account
    Mint { amount: u64 },
    /// Burn tokens from an account
    /// Accounts expected:
    /// 0. [signer] The account burning tokens
    /// 1. [writable] The token account to burn from
    /// 2. [writable] The mint account
    Burn { amount: u64 },
}

// Token account data structure
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct TokenAccount {
    pub owner: Pubkey,
    pub amount: u64,
    pub mint: Pubkey,
}

// Mint account data structure
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct MintAccount {
    pub supply: u64,
    pub decimals: u8,
    pub mint_authority: Pubkey,
    pub freeze_authority: Option<Pubkey>,
}

// Main program entry point
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = TokenInstruction::try_from_slice(instruction_data)
        .map_err(|_| ProgramError::InvalidInstructionData)?;

    match instruction {
        TokenInstruction::Initialize {
            name,
            symbol,
            decimals,
            initial_supply,
        } => {
            msg!("Initializing token: {} ({})", name, symbol);
            initialize_token(accounts, name, symbol, decimals, initial_supply)
        }
        TokenInstruction::Transfer { amount } => {
            msg!("Transferring {} tokens", amount);
            transfer_tokens(accounts, amount)
        }
        TokenInstruction::Mint { amount } => {
            msg!("Minting {} tokens", amount);
            mint_tokens(accounts, amount)
        }
        TokenInstruction::Burn { amount } => {
            msg!("Burning {} tokens", amount);
            burn_tokens(accounts, amount)
        }
    }
}

2. Token Implementation Functions

rust
// Initialize a new token
fn initialize_token(
    accounts: &[AccountInfo],
    name: String,
    symbol: String,
    decimals: u8,
    initial_supply: u64,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let owner = next_account_info(account_info_iter)?;
    let token_account = next_account_info(account_info_iter)?;

    // Verify the owner is signing
    if !owner.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Create token account data
    let token_data = TokenAccount {
        owner: *owner.key,
        amount: initial_supply,
        mint: *token_account.key,
    };

    // Serialize and store the token account data
    token_data.serialize(&mut &mut token_account.data.borrow_mut()[..])?;

    msg!("Token {} ({}) initialized with supply: {}", name, symbol, initial_supply);
    Ok(())
}

// Transfer tokens between accounts
fn transfer_tokens(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let from = next_account_info(account_info_iter)?;
    let from_token_account = next_account_info(account_info_iter)?;
    let to_token_account = next_account_info(account_info_iter)?;

    // Verify the sender is signing
    if !from.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Deserialize token accounts
    let mut from_data = TokenAccount::try_from_slice(&from_token_account.data.borrow())?;
    let mut to_data = TokenAccount::try_from_slice(&to_token_account.data.borrow())?;

    // Check if sender has enough tokens
    if from_data.amount < amount {
        return Err(ProgramError::InsufficientFunds);
    }

    // Perform the transfer
    from_data.amount -= amount;
    to_data.amount += amount;

    // Update the accounts
    from_data.serialize(&mut &mut from_token_account.data.borrow_mut()[..])?;
    to_data.serialize(&mut &mut to_token_account.data.borrow_mut()[..])?;

    msg!("Transferred {} tokens from {} to {}",
         amount, from.key, to_token_account.key);
    Ok(())
}

// Mint new tokens
fn mint_tokens(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let mint_authority = next_account_info(account_info_iter)?;
    let token_account = next_account_info(account_info_iter)?;
    let mint_account = next_account_info(account_info_iter)?;

    // Verify the mint authority is signing
    if !mint_authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Deserialize accounts
    let mut token_data = TokenAccount::try_from_slice(&token_account.data.borrow())?;
    let mut mint_data = MintAccount::try_from_slice(&mint_account.data.borrow())?;

    // Update token account
    token_data.amount += amount;
    token_data.serialize(&mut &mut token_account.data.borrow_mut()[..])?;

    // Update mint account
    mint_data.supply += amount;
    mint_data.serialize(&mut &mut mint_account.data.borrow_mut()[..])?;

    msg!("Minted {} tokens to {}", amount, token_account.key);
    Ok(())
}

// Burn tokens
fn burn_tokens(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let burner = next_account_info(account_info_iter)?;
    let token_account = next_account_info(account_info_iter)?;
    let mint_account = next_account_info(account_info_iter)?;

    // Verify the burner is signing
    if !burner.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Deserialize accounts
    let mut token_data = TokenAccount::try_from_slice(&token_account.data.borrow())?;
    let mut mint_data = MintAccount::try_from_slice(&mint_account.data.borrow())?;

    // Check if account has enough tokens to burn
    if token_data.amount < amount {
        return Err(ProgramError::InsufficientFunds);
    }

    // Perform the burn
    token_data.amount -= amount;
    mint_data.supply -= amount;

    // Update the accounts
    token_data.serialize(&mut &mut token_account.data.borrow_mut()[..])?;
    mint_data.serialize(&mut &mut mint_account.data.borrow_mut()[..])?;

    msg!("Burned {} tokens from {}", amount, token_account.key);
    Ok(())
}

3. Error Handling

rust
// src/error.rs
use thiserror::Error;

#[derive(Error, Debug, Copy, Clone)]
pub enum TokenError {
    #[error("Insufficient funds")]
    InsufficientFunds,
    #[error("Invalid mint authority")]
    InvalidMintAuthority,
    #[error("Invalid freeze authority")]
    InvalidFreezeAuthority,
    #[error("Amount must be greater than zero")]
    InvalidAmount,
    #[error("Token account not initialized")]
    UninitializedAccount,
}

impl From<TokenError> for ProgramError {
    fn from(e: TokenError) -> Self {
        ProgramError::Custom(e as u32)
    }
}

Testing the Token

1. Unit Tests

rust
// tests/token_tests.rs
use simple_token::*;
use solana_program_test::*;
use solana_sdk::{
    account::Account,
    instruction::{AccountMeta, Instruction},
    pubkey::Pubkey,
    signature::Signer,
    transaction::Transaction,
};

#[tokio::test]
async fn test_initialize_token() {
    let program_id = Pubkey::new_unique();
    let mut program_test = ProgramTest::new(
        "simple_token",
        program_id,
        processor!(process_instruction),
    );

    let (mut banks_client, payer, recent_blockhash) = program_test.start().await;

    // Create accounts
    let owner = Keypair::new();
    let token_account = Keypair::new();

    // Create instruction
    let instruction = Instruction {
        program_id,
        accounts: vec![
            AccountMeta::new(owner.pubkey(), true),
            AccountMeta::new(token_account.pubkey(), false),
        ],
        data: TokenInstruction::Initialize {
            name: "My Token".to_string(),
            symbol: "MTK".to_string(),
            decimals: 9,
            initial_supply: 1000000,
        }
        .try_to_vec()
        .unwrap(),
    };

    // Send transaction
    let transaction = Transaction::new_signed_with_payer(
        &[instruction],
        Some(&payer.pubkey()),
        &[&payer, &owner, &token_account],
        recent_blockhash,
    );

    banks_client.process_transaction(transaction).await.unwrap();

    // Verify token account was initialized
    let account = banks_client
        .get_account(token_account.pubkey())
        .await
        .unwrap()
        .unwrap();

    let token_data = TokenAccount::try_from_slice(&account.data).unwrap();
    assert_eq!(token_data.amount, 1000000);
    assert_eq!(token_data.owner, owner.pubkey());
}

2. Integration Tests

rust
#[tokio::test]
async fn test_transfer_tokens() {
    // Setup similar to above...

    // Initialize token
    // ... initialization code ...

    // Create transfer instruction
    let transfer_instruction = Instruction {
        program_id,
        accounts: vec![
            AccountMeta::new(owner.pubkey(), true),
            AccountMeta::new(from_token_account.pubkey(), false),
            AccountMeta::new(to_token_account.pubkey(), false),
        ],
        data: TokenInstruction::Transfer { amount: 100 }
            .try_to_vec()
            .unwrap(),
    };

    // Execute transfer
    let transfer_transaction = Transaction::new_signed_with_payer(
        &[transfer_instruction],
        Some(&payer.pubkey()),
        &[&payer, &owner],
        recent_blockhash,
    );

    banks_client.process_transaction(transfer_transaction).await.unwrap();

    // Verify transfer
    let from_account = banks_client
        .get_account(from_token_account.pubkey())
        .await
        .unwrap()
        .unwrap();

    let from_data = TokenAccount::try_from_slice(&from_account.data).unwrap();
    assert_eq!(from_data.amount, 999900); // 1000000 - 100
}

Building and Deploying

1. Build the Program

bash
# Build for Solana
cargo build-bpf

# Or using Solana CLI
solana program build

2. Deploy to Localnet

bash
# Start local Solana cluster
solana-test-validator

# Deploy the program
solana program deploy target/deploy/simple_token.so

# Get program ID
solana program show <PROGRAM_ID>

3. Deploy to Devnet

bash
# Set cluster to devnet
solana config set --url devnet

# Airdrop SOL for deployment
solana airdrop 2

# Deploy program
solana program deploy target/deploy/simple_token.so

Frontend Integration

1. JavaScript Client

javascript
// client.js
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";

const connection = new Connection("https://api.devnet.solana.com");
const programId = new PublicKey("YOUR_PROGRAM_ID");

// Create token initialization instruction
async function initializeToken(
  owner,
  tokenAccount,
  name,
  symbol,
  decimals,
  supply
) {
  const instruction = new TransactionInstruction({
    keys: [
      { pubkey: owner.publicKey, isSigner: true, isWritable: false },
      { pubkey: tokenAccount.publicKey, isSigner: false, isWritable: true },
    ],
    programId: programId,
    data: Buffer.from(/* serialized TokenInstruction::Initialize */),
  });

  const transaction = new Transaction().add(instruction);
  return await connection.sendTransaction(transaction, [owner, tokenAccount]);
}

// Transfer tokens
async function transferTokens(from, fromTokenAccount, toTokenAccount, amount) {
  const instruction = new TransactionInstruction({
    keys: [
      { pubkey: from.publicKey, isSigner: true, isWritable: false },
      { pubkey: fromTokenAccount.publicKey, isSigner: false, isWritable: true },
      { pubkey: toTokenAccount.publicKey, isSigner: false, isWritable: true },
    ],
    programId: programId,
    data: Buffer.from(/* serialized TokenInstruction::Transfer */),
  });

  const transaction = new Transaction().add(instruction);
  return await connection.sendTransaction(transaction, [from]);
}

Advanced Features

1. Token Metadata

rust
// Add metadata to token
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct TokenMetadata {
    pub name: String,
    pub symbol: String,
    pub description: String,
    pub image: String,
    pub decimals: u8,
    pub properties: Vec<(String, String)>,
}

2. Access Control

rust
// Add role-based access control
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum Role {
    Admin,
    Minter,
    Burner,
    User,
}

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct AccessControl {
    pub roles: std::collections::HashMap<Pubkey, Role>,
}

3. Events and Logging

rust
// Add event logging
use solana_program::log::sol_log;

fn log_event(event: &str) {
    sol_log(event);
}

// Usage
log_event("Token minted successfully");

Best Practices

1. Security

  • Always validate input parameters
  • Check account ownership and permissions
  • Use proper error handling
  • Implement reentrancy protection
  • Validate account data before deserialization

2. Gas Optimization

  • Minimize storage operations
  • Use efficient data structures
  • Batch operations when possible
  • Avoid unnecessary computations

3. Testing

  • Write comprehensive unit tests
  • Test edge cases and error conditions
  • Use integration tests for full workflows
  • Test with different account configurations

Common Pitfalls

1. Account Validation

rust
// ❌ Don't forget to validate accounts
fn bad_function(accounts: &[AccountInfo]) -> ProgramResult {
    let account = &accounts[0];
    // Missing validation!
    Ok(())
}

// ✅ Always validate accounts
fn good_function(accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let account = next_account_info(account_info_iter)?;

    if !account.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    Ok(())
}

2. Data Serialization

rust
// ❌ Incorrect serialization
account.data.borrow_mut()[..] = data;

// ✅ Proper serialization
data.serialize(&mut &mut account.data.borrow_mut()[..])?;

Conclusion

Building a token with Rust on Solana is a powerful way to create efficient, secure blockchain applications. The combination of Rust's safety guarantees and Solana's high-performance architecture makes it an excellent choice for token development.

Key takeaways:

  1. Start Simple: Begin with basic functionality and add complexity gradually
  2. Test Thoroughly: Comprehensive testing is crucial for blockchain applications
  3. Security First: Always validate inputs and check permissions
  4. Optimize Carefully: Balance functionality with gas efficiency
  5. Document Everything: Clear documentation helps with maintenance and updates

The token we built includes core functionality like minting, transferring, and burning. You can extend it with additional features like metadata, access control, and advanced economics.

Ready to build something amazing? Start with this foundation and gradually add the features your project needs!


Want to learn more about advanced Rust blockchain development? Check out our next guide on building DeFi protocols!