Token Standards: ERC-20, ERC-721, ERC-1155

Implementing token standards for fungible tokens, NFTs, and multi-token contracts.

Advanced⏱️ 55 min📚 Prerequisites: 2

Token Standards: ERC-20, ERC-721, ERC-1155

Token standards define interfaces for creating and managing tokens on blockchains.

ERC-20: Fungible Tokens

ERC-20 is the standard for fungible tokens (interchangeable, like currency).

Core Functions

RUST
trait ERC20 {
    fn total_supply(&self) -> u64;
    fn balance_of(&self, owner: &str) -> u64;
    fn transfer(&mut self, to: &str, amount: u64) -> Result<(), String>;
    fn transfer_from(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String>;
    fn approve(&mut self, spender: &str, amount: u64) -> Result<(), String>;
    fn allowance(&self, owner: &str, spender: &str) -> u64;
}

Events

  • Transfer(from, to, amount): Token transfer
  • Approval(owner, spender, amount): Approval granted

ERC-721: Non-Fungible Tokens (NFTs)

ERC-721 is the standard for NFTs (unique, non-interchangeable tokens).

Core Functions

RUST
trait ERC721 {
    fn balance_of(&self, owner: &str) -> u64;
    fn owner_of(&self, token_id: u64) -> Option<String>;
    fn transfer_from(&mut self, from: &str, to: &str, token_id: u64) -> Result<(), String>;
    fn approve(&mut self, approved: &str, token_id: u64) -> Result<(), String>;
    fn get_approved(&self, token_id: u64) -> Option<String>;
    fn set_approval_for_all(&mut self, operator: &str, approved: bool) -> Result<(), String>;
    fn is_approved_for_all(&self, owner: &str, operator: &str) -> bool;
    fn safe_transfer_from(&mut self, from: &str, to: &str, token_id: u64) -> Result<(), String>;
}

Metadata

NFTs include metadata (name, description, image URI) typically stored off-chain.

ERC-1155: Multi-Token Standard

ERC-1155 supports both fungible and non-fungible tokens in a single contract.

Core Functions

RUST
trait ERC1155 {
    fn balance_of(&self, account: &str, token_id: u64) -> u64;
    fn balance_of_batch(&self, accounts: &[&str], token_ids: &[u64]) -> Vec<u64>;
    fn set_approval_for_all(&mut self, operator: &str, approved: bool) -> Result<(), String>;
    fn is_approved_for_all(&self, owner: &str, operator: &str) -> bool;
    fn safe_transfer_from(
        &mut self,
        from: &str,
        to: &str,
        token_id: u64,
        amount: u64,
    ) -> Result<(), String>;
    fn safe_batch_transfer_from(
        &mut self,
        from: &str,
        to: &str,
        token_ids: &[u64],
        amounts: &[u64],
    ) -> Result<(), String>;
}

Implementation Example

RUST
use std::collections::HashMap;

struct ERC20Token {
    name: String,
    symbol: String,
    decimals: u8,
    total_supply: u64,
    balances: HashMap<String, u64>,
    allowances: HashMap<(String, String), u64>,
}

impl ERC20Token {
    fn new(name: String, symbol: String, initial_supply: u64) -> Self {
        let mut balances = HashMap::new();
        balances.insert(String::from("owner"), initial_supply);
        
        ERC20Token {
            name,
            symbol,
            decimals: 18,
            total_supply: initial_supply,
            balances,
            allowances: HashMap::new(),
        }
    }
    
    fn transfer(&mut self, to: &str, amount: u64) -> Result<(), String> {
        let from = String::from("owner"); // Simplified
        self._transfer(&from, to, amount)
    }
    
    fn _transfer(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String> {
        let from_balance = *self.balances.get(from).unwrap_or(&0);
        if from_balance < amount {
            return Err(String::from("Insufficient balance"));
        }
        
        *self.balances.entry(from.to_string()).or_insert(0) -= amount;
        *self.balances.entry(to.to_string()).or_insert(0) += amount;
        
        Ok(())
    }
    
    fn approve(&mut self, spender: &str, amount: u64) -> Result<(), String> {
        let owner = String::from("owner");
        self.allowances.insert((owner, spender.to_string()), amount);
        Ok(())
    }
    
    fn transfer_from(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String> {
        let spender = String::from("spender"); // Simplified
        let allowance = *self.allowances.get(&(from.to_string(), spender.clone())).unwrap_or(&0);
        
        if allowance < amount {
            return Err(String::from("Insufficient allowance"));
        }
        
        self.allowances.insert((from.to_string(), spender), allowance - amount);
        self._transfer(from, to, amount)
    }
}

Use Cases

ERC-20

  • Stablecoins: USDC, DAI
  • Governance Tokens: UNI, COMP
  • Utility Tokens: In-app currencies

ERC-721

  • Digital Art: CryptoPunks, Bored Apes
  • Collectibles: Trading cards, virtual items
  • Identity: Domain names, certificates

ERC-1155

  • Gaming: Multiple item types in one contract
  • Marketplaces: Efficient batch transfers
  • Hybrid: Mix of fungible and non-fungible

Best Practices

  • Reentrancy Protection: Use checks-effects-interactions pattern
  • Integer Overflow: Use checked arithmetic
  • Access Control: Implement proper permissions
  • Events: Emit events for all state changes
  • Gas Optimization: Batch operations when possible

Security Considerations

  • Overflow/Underflow: Use safe math libraries
  • Reentrancy: Guard against recursive calls
  • Access Control: Verify caller permissions
  • Input Validation: Check all parameters

Code Examples

ERC-20 Implementation

Complete ERC-20 token implementation

RUST
use std::collections::HashMap;

struct ERC20Token {
    name: String,
    symbol: String,
    total_supply: u64,
    balances: HashMap<String, u64>,
    allowances: HashMap<(String, String), u64>,
}

impl ERC20Token {
    fn new(name: String, symbol: String, initial_supply: u64) -> Self {
        let mut balances = HashMap::new();
        balances.insert(String::from("owner"), initial_supply);
        
        ERC20Token {
            name,
            symbol,
            total_supply: initial_supply,
            balances,
            allowances: HashMap::new(),
        }
    }
    
    fn total_supply(&self) -> u64 {
        self.total_supply
    }
    
    fn balance_of(&self, owner: &str) -> u64 {
        *self.balances.get(owner).unwrap_or(&0)
    }
    
    fn transfer(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String> {
        let balance = self.balance_of(from);
        if balance < amount {
            return Err(String::from("Insufficient balance"));
        }
        
        *self.balances.entry(from.to_string()).or_insert(0) -= amount;
        *self.balances.entry(to.to_string()).or_insert(0) += amount;
        
        Ok(())
    }
    
    fn approve(&mut self, owner: &str, spender: &str, amount: u64) -> Result<(), String> {
        self.allowances.insert((owner.to_string(), spender.to_string()), amount);
        Ok(())
    }
    
    fn allowance(&self, owner: &str, spender: &str) -> u64 {
        *self.allowances.get(&(owner.to_string(), spender.to_string())).unwrap_or(&0)
    }
    
    fn transfer_from(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String> {
        let spender = String::from("spender"); // Simplified
        let allowance = self.allowance(from, &spender);
        
        if allowance < amount {
            return Err(String::from("Insufficient allowance"));
        }
        
        self.approve(from, &spender, allowance - amount)?;
        self.transfer(from, to, amount)
    }
}

fn main() {
    let mut token = ERC20Token::new(
        String::from("MyToken"),
        String::from("MTK"),
        1000000,
    );
    
    println!("Token: {} ({})", token.name, token.symbol);
    println!("Total supply: {}", token.total_supply());
    println!("Owner balance: {}", token.balance_of("owner"));
    
    // Transfer tokens
    token.transfer("owner", "alice", 1000).unwrap();
    println!("Alice balance: {}", token.balance_of("alice"));
    
    // Approve and transfer_from
    token.approve("alice", "bob", 500).unwrap();
    println!("Allowance: {}", token.allowance("alice", "bob"));
}

Explanation:

ERC-20 defines the standard interface for fungible tokens. Key features: transfer, approve (for delegated transfers), and balance tracking. This enables tokens to work with wallets, exchanges, and DeFi protocols.

ERC-721 NFT

Basic ERC-721 NFT implementation

RUST
use std::collections::HashMap;

struct ERC721Token {
    name: String,
    symbol: String,
    token_owners: HashMap<u64, String>,
    balances: HashMap<String, u64>,
    token_approvals: HashMap<u64, String>,
    operator_approvals: HashMap<(String, String), bool>,
    next_token_id: u64,
}

impl ERC721Token {
    fn new(name: String, symbol: String) -> Self {
        ERC721Token {
            name,
            symbol,
            token_owners: HashMap::new(),
            balances: HashMap::new(),
            token_approvals: HashMap::new(),
            operator_approvals: HashMap::new(),
            next_token_id: 1,
        }
    }
    
    fn mint(&mut self, to: &str) -> u64 {
        let token_id = self.next_token_id;
        self.next_token_id += 1;
        
        self.token_owners.insert(token_id, to.to_string());
        *self.balances.entry(to.to_string()).or_insert(0) += 1;
        
        token_id
    }
    
    fn balance_of(&self, owner: &str) -> u64 {
        *self.balances.get(owner).unwrap_or(&0)
    }
    
    fn owner_of(&self, token_id: u64) -> Option<&String> {
        self.token_owners.get(&token_id)
    }
    
    fn transfer_from(&mut self, from: &str, to: &str, token_id: u64) -> Result<(), String> {
        let owner = self.owner_of(token_id)
            .ok_or(String::from("Token does not exist"))?;
        
        if owner != from {
            return Err(String::from("Not the owner"));
        }
        
        // Remove from old owner
        self.token_owners.insert(token_id, to.to_string());
        *self.balances.entry(from.to_string()).or_insert(1) -= 1;
        *self.balances.entry(to.to_string()).or_insert(0) += 1;
        
        // Clear approval
        self.token_approvals.remove(&token_id);
        
        Ok(())
    }
    
    fn approve(&mut self, approved: &str, token_id: u64) -> Result<(), String> {
        let owner = self.owner_of(token_id)
            .ok_or(String::from("Token does not exist"))?;
        
        self.token_approvals.insert(token_id, approved.to_string());
        Ok(())
    }
}

fn main() {
    let mut nft = ERC721Token::new(
        String::from("MyNFT"),
        String::from("MNFT"),
    );
    
    // Mint NFTs
    let token1 = nft.mint("alice");
    let token2 = nft.mint("alice");
    let token3 = nft.mint("bob");
    
    println!("Minted tokens: {}, {}, {}", token1, token2, token3);
    println!("Alice balance: {}", nft.balance_of("alice"));
    println!("Bob balance: {}", nft.balance_of("bob"));
    
    // Transfer NFT
    nft.transfer_from("alice", "bob", token1).unwrap();
    println!("After transfer - Alice balance: {}", nft.balance_of("alice"));
    println!("After transfer - Bob balance: {}", nft.balance_of("bob"));
}

Explanation:

ERC-721 defines the standard for NFTs. Each token has a unique ID and owner. NFTs can be transferred, approved for others to transfer, and tracked individually. This enables digital collectibles, art, and unique assets.

Exercises

Simple Token

Create a simple token with transfer functionality!

Medium

Starter Code:

RUST
use std::collections::HashMap;

struct Token {
    balances: HashMap<String, u64>,
}

fn main() {
    // Create token
    // Transfer tokens
}