Skip to main content
The Soroban SDK provides standardized interfaces for interacting with token contracts, including the Stellar Asset Contract (SAC). These interfaces follow the SEP-41 token standard.

TokenClient

The TokenClient provides a type-safe interface for calling token contracts:
use soroban_sdk::{token::Client as TokenClient, Address, Env};

let env = Env::default();
let token_address = Address::generate(&env);
let token = TokenClient::new(&env, &token_address);

Token Interface Methods

Querying Token Information

use soroban_sdk::{token::Client as TokenClient, String};

// Get token metadata
let decimals: u32 = token.decimals();
let name: String = token.name();
let symbol: String = token.symbol();

// Get account balance
let balance: i128 = token.balance(&account_address);

Transfers

use soroban_sdk::MuxedAddress;

// Transfer tokens
token.transfer(
    &from,
    &to,  // Can be Address or MuxedAddress
    &amount
);

// Transfer with muxed address (includes memo)
let to_muxed = MuxedAddress::new(&env, &to_address, 12345u64);
token.transfer(&from, &to_muxed, &amount);

Allowances

Implement approval and delegated transfer patterns:
// Approve spender to use tokens
let expiration_ledger = env.ledger().sequence() + 100;
token.approve(
    &owner,
    &spender,
    &amount,
    &expiration_ledger
);

// Check allowance
let allowance: i128 = token.allowance(&owner, &spender);

// Transfer using allowance
token.transfer_from(
    &spender,  // The authorized spender
    &owner,    // Owner of the tokens
    &recipient,
    &amount
);

Burning Tokens

// Burn tokens from account
token.burn(&from, &amount);

// Burn using allowance
token.burn_from(&spender, &from, &amount);

Stellar Asset Contract

The Stellar Asset Contract (SAC) is the built-in token implementation for Stellar assets. It extends the basic token interface with administrative functions.

Using StellarAssetClient

use soroban_sdk::token::StellarAssetClient;

let sac = StellarAssetClient::new(&env, &token_address);

// All TokenClient methods, plus:

// Admin functions
let admin = sac.admin();
sac.set_admin(&new_admin);

// Authorization control
let is_authorized = sac.authorized(&account);
sac.set_authorized(&account, &true);

// Minting (admin only)
sac.mint(&recipient, &amount);

// Clawback (admin only)
sac.clawback(&from, &amount);

Implementing Token Functionality

Creating a Custom Token

use soroban_sdk::{
    contract, contractimpl, contracttype, token::TokenInterface,
    Address, Env, String
};

#[contract]
pub struct CustomToken;

#[contractimpl]
impl TokenInterface for CustomToken {
    fn transfer(env: Env, from: Address, to: Address, amount: i128) {
        from.require_auth();
        
        // Update balances
        let mut from_balance = get_balance(&env, &from);
        let mut to_balance = get_balance(&env, &to);
        
        from_balance -= amount;
        to_balance += amount;
        
        set_balance(&env, &from, from_balance);
        set_balance(&env, &to, to_balance);
        
        // Emit transfer event
        TokenEvents::new(&env).transfer(from, to, amount);
    }
    
    fn balance(env: Env, id: Address) -> i128 {
        get_balance(&env, &id)
    }
    
    fn decimals(env: Env) -> u32 {
        7  // Default for Stellar assets
    }
    
    fn name(env: Env) -> String {
        String::from_str(&env, "My Token")
    }
    
    fn symbol(env: Env) -> String {
        String::from_str(&env, "MTK")
    }
    
    // Implement other required methods...
}

Token Utilities (soroban-token-sdk)

The soroban-token-sdk crate provides helper utilities for token contracts.

Token Metadata

use soroban_token_sdk::{TokenUtils, metadata::{TokenMetadata, Metadata}};
use soroban_sdk::String;

let token_utils = TokenUtils::new(&env);

// Set token metadata
let metadata = TokenMetadata {
    decimal: 7,
    name: String::from_str(&env, "My Token"),
    symbol: String::from_str(&env, "MTK"),
};
token_utils.metadata().set_metadata(&metadata);

// Get token metadata
let stored = token_utils.metadata().get_metadata();

Token Events

Use standardized event types for token operations:
use soroban_token_sdk::events;
use soroban_sdk::contractevent;

// Transfer event
events::Transfer {
    from: from_address.clone(),
    to: to_address.clone(),
    amount,
}.publish(&env);

// Mint event
events::Mint {
    to: recipient.clone(),
    amount,
}.publish(&env);

// Burn event
events::Burn {
    from: from_address.clone(),
    amount,
}.publish(&env);

// Approve event
events::Approve {
    from: owner.clone(),
    to: spender.clone(),
    amount,
    expiration_ledger,
}.publish(&env);

// Clawback event
events::Clawback {
    from: from_address.clone(),
    amount,
}.publish(&env);

Testing with Tokens

Registering Token Contracts

#[cfg(test)]
mod tests {
    use super::*;
    use soroban_sdk::{testutils::Address as _, Address};
    
    #[test]
    fn test_token_transfer() {
        let env = Env::default();
        
        // Register Stellar Asset Contract
        let admin = Address::generate(&env);
        let sac = env.register_stellar_asset_contract_v2(admin.clone());
        let token_address = sac.address();
        
        // Create token client
        let token = TokenClient::new(&env, &token_address);
        
        // Test token operations
        let user = Address::generate(&env);
        let amount = 1000i128;
        
        sac.mint(&user, &amount);
        assert_eq!(token.balance(&user), amount);
    }
}

Mock Authentication

use soroban_sdk::testutils::{MockAuth, MockAuthInvoke};

#[test]
fn test_with_auth() {
    let env = Env::default();
    let token = TokenClient::new(&env, &token_address);
    
    let from = Address::generate(&env);
    let to = Address::generate(&env);
    
    // Mock authentication for the transfer
    token.mock_auths(&[MockAuth {
        address: &from,
        invoke: &MockAuthInvoke {
            contract: &token_address,
            fn_name: "transfer",
            args: (&from, &to, 100i128).into_val(&env),
            sub_invokes: &[],
        },
    }]).transfer(&from, &to, &100i128);
}

Setting Issuer Flags

use soroban_sdk::testutils::IssuerFlags;

#[test]
fn test_issuer_flags() {
    let env = Env::default();
    let admin = Address::generate(&env);
    let sac = env.register_stellar_asset_contract_v2(admin);
    
    // Set authorization required
    sac.issuer().set_flag(IssuerFlags::RequiredFlag);
    
    // Set revocable
    sac.issuer().set_flag(IssuerFlags::RevocableFlag);
    
    // Check flags
    let flags = sac.issuer().flags();
    assert_eq!(
        flags,
        (IssuerFlags::RequiredFlag as u32) | (IssuerFlags::RevocableFlag as u32)
    );
}

DeFi Patterns

Token Swaps

#[contract]
pub struct SwapContract;

#[contractimpl]
impl SwapContract {
    pub fn swap(
        env: Env,
        token_a: Address,
        token_b: Address,
        amount_a: i128,
        min_amount_b: i128,
        user: Address,
    ) -> i128 {
        user.require_auth();
        
        let token_a_client = TokenClient::new(&env, &token_a);
        let token_b_client = TokenClient::new(&env, &token_b);
        
        // Transfer token A from user to contract
        token_a_client.transfer(&user, &env.current_contract_address(), &amount_a);
        
        // Calculate swap amount
        let amount_b = calculate_swap(&env, &token_a, &token_b, amount_a);
        require!(amount_b >= min_amount_b, Error::SlippageExceeded);
        
        // Transfer token B to user
        token_b_client.transfer(&env.current_contract_address(), &user, &amount_b);
        
        amount_b
    }
}

Staking Contract

#[contractimpl]
impl StakingContract {
    pub fn stake(env: Env, user: Address, amount: i128) {
        user.require_auth();
        
        let stake_token = get_stake_token(&env);
        let token = TokenClient::new(&env, &stake_token);
        
        // Transfer tokens to contract
        token.transfer(&user, &env.current_contract_address(), &amount);
        
        // Update stake balance
        let current_stake = get_stake_balance(&env, &user);
        set_stake_balance(&env, &user, current_stake + amount);
    }
    
    pub fn unstake(env: Env, user: Address, amount: i128) {
        user.require_auth();
        
        let stake_balance = get_stake_balance(&env, &user);
        require!(stake_balance >= amount, Error::InsufficientStake);
        
        // Update stake balance
        set_stake_balance(&env, &user, stake_balance - amount);
        
        // Return tokens to user
        let stake_token = get_stake_token(&env);
        let token = TokenClient::new(&env, &stake_token);
        token.transfer(&env.current_contract_address(), &user, &amount);
    }
}

Best Practices

Always Require Authentication

// ✅ Good: Requires auth before transfer
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
    from.require_auth();
    // ... perform transfer
}

// ❌ Bad: No auth check
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
    // ... perform transfer - anyone can call!
}

Check Balances Before Operations

pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
    from.require_auth();
    
    let balance = get_balance(&env, &from);
    require!(balance >= amount, Error::InsufficientBalance);
    
    // Perform transfer...
}

Emit Events for All State Changes

use soroban_token_sdk::events;

pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
    from.require_auth();
    
    // Update balances...
    
    // Always emit events
    events::Transfer { from, to, amount }.publish(&env);
}

Handle Muxed Addresses

use soroban_sdk::MuxedAddress;

pub fn transfer(env: Env, from: Address, to: MuxedAddress, amount: i128) {
    // Extract the underlying address
    let to_address = to.address();
    
    // Optional: Get muxed ID for memo
    let memo_id = to.muxed_id();  // Returns Option<u64>
    
    // Perform transfer to the address...
}

SEP-41 Compliance

To be SEP-41 compliant, implement all required interface methods:
  • allowance()
  • approve()
  • balance()
  • transfer()
  • transfer_from()
  • burn()
  • burn_from()
  • decimals()
  • name()
  • symbol()