DEV Community

yuzurush
yuzurush

Posted on • Edited on

Soroban Contracts 101 : Tokens (Part 1)

Hi there! Welcome to my tenth post of my series called "Soroban Contracts 101", where I'll be explaining the basics of Soroban contracts, such as data storage, authentication, custom types, and more. All the code that we're gonna explain throughout this series will mostly come from soroban-contracts-101 github repository.

In this tenth post of the series, I'll be covering soroban token contract. With this contract we can do various things such as initialize or create our own token, mint it to an address, check balance of specified token on specified account, and more. I will divide this post into two part since this is gonna be a long journey.

The Contract Code

This contract has several modules, which are:

  • admin.rs

This module contains admin logic (checking admin, setting admin)

use crate::storage_types::DataKey;
use soroban_sdk::{Address, Env};

pub fn has_administrator(e: &Env) -> bool {
    let key = DataKey::Admin;
    e.storage().has(&key)
}

pub fn read_administrator(e: &Env) -> Address {
    let key = DataKey::Admin;
    e.storage().get_unchecked(&key).unwrap()
}

pub fn write_administrator(e: &Env, id: &Address) {
    let key = DataKey::Admin;
    e.storage().set(&key, id);
}
Enter fullscreen mode Exit fullscreen mode

This module imports DataKey from storage_types.rs and types from soroban_sdk.
It defines four admin-related functions:

  1. has_administrator - Checks if an admin is set. It uses a DataKey of type Admin and the storage has method to check.
  2. read_administrator - Reads the admin address. It uses a DataKey of type Admin and the storage get_unchecked method (panicking if no admin is set).
  3. write_administrator - Writes an admin address. It uses a DataKey of type Admin and the storage set method.

This module contains the core logic for managing an admin address for the token contract. It allows checking, reading and writing the admin address, and checking admin authorization for functions.

  • allowance.rs

Contains allowance logic (reading, increasing, decreasing allowance)

use crate::storage_types::{AllowanceDataKey, DataKey};
use soroban_sdk::{Address, Env};

pub fn read_allowance(e: &Env, from: Address, spender: Address) -> i128 {
    let key = DataKey::Allowance(AllowanceDataKey { from, spender });
    if let Some(allowance) = e.storage().get(&key) {
        allowance.unwrap()
    } else {
        0
    }
}

pub fn write_allowance(e: &Env, from: Address, spender: Address, amount: i128) {
    let key = DataKey::Allowance(AllowanceDataKey { from, spender });
    e.storage().set(&key, &amount);
}

pub fn spend_allowance(e: &Env, from: Address, spender: Address, amount: i128) {
    let allowance = read_allowance(e, from.clone(), spender.clone());
    if allowance < amount {
        panic!("insufficient allowance");
    }
    write_allowance(e, from, spender, allowance - amount);
}
Enter fullscreen mode Exit fullscreen mode

This module imports types from storage_types.rs, including AllowanceDataKey and DataKey. These are types used to store allowance data.
It defines three allowance-related functions:

  1. read_allowance - Reads the allowance for a spender on behalf of a from address. It uses a DataKey of type AllowanceDataKey to look up the data in contract storage, and returns 0 if no allowance is set.
  2. write_allowance - Writes an allowance amount for a spender on behalf of a from address. It uses a DataKey of type AllowanceDataKey to store the data.
  3. spend_allowance - Spends some amount from an allowance. It first reads the current allowance, checks that it is sufficient, then decreases it by the amount and writes the new allowance. It panics if there is insufficient allowance.

This module contains the core logic for managing allowances in the token contract. It uses storage types from another module and functions for reading/writing from storage to implement the allowance logic.

  • balance.rs

Contains balance logic (reading, spending, receiving balance)

use crate::storage_types::DataKey;
use soroban_sdk::{Address, Env};

pub fn read_balance(e: &Env, addr: Address) -> i128 {
    let key = DataKey::Balance(addr);
    if let Some(balance) = e.storage().get(&key) {
        balance.unwrap()
    } else {
        0
    }
}

fn write_balance(e: &Env, addr: Address, amount: i128) {
    let key = DataKey::Balance(addr);
    e.storage().set(&key, &amount);
}

pub fn receive_balance(e: &Env, addr: Address, amount: i128) {
    let balance = read_balance(e, addr.clone());
    if !is_authorized(e, addr.clone()) {
        panic!("can't receive when deauthorized");
    }
    write_balance(e, addr, balance + amount);
}

pub fn spend_balance(e: &Env, addr: Address, amount: i128) {
    let balance = read_balance(e, addr.clone());
    if !is_authorized(e, addr.clone()) {
        panic!("can't spend when deauthorized");
    }
    if balance < amount {
        panic!("insufficient balance");
    }
    write_balance(e, addr, balance - amount);
}

pub fn is_authorized(e: &Env, addr: Address) -> bool {
    let key = DataKey::State(addr);
    if let Some(state) = e.storage().get(&key) {
        state.unwrap()
    } else {
        true
    }
}

pub fn write_authorization(e: &Env, addr: Address, is_authorized: bool) {
    let key = DataKey::State(addr);
    e.storage().set(&key, &is_authorized);
}
Enter fullscreen mode Exit fullscreen mode

It imports DataKey from storage_types.rs and types from soroban_sdk.
It defines six balance-related functions:

  1. read_balance - Reads the balance for an address. It uses a DataKey of type Balance and the storage get method, returning 0 if no balance is set.
  2. write_balance - Writes a balance for an address. It uses a DataKey of type Balance and the storage set method.
  3. receive_balance - Increases the balance for an address by some amount. It first reads the current balance, checks authorization status, then increases and writes the new balance. It panics if deauthorized.
  4. spend_balance - Decreases the balance for an address by some amount. It first reads the current balance, checks authorization status and sufficient balance, then decreases and writes the new balance. It panics if deauthorized or if insufficient balance.
  5. is_authorized - Checks the authorization status for an address. It uses a DataKey of type State and the storage get method, defaulting to true if no state is set.
  6. write_authorization - Writes the authorization status for an address. It uses a DataKey of type State and the storage set method.

So in summary, this module contains the core logic for managing balances (and authorization status) in the token contract. It uses storage types from another module and functions for reading/writing from storage to implement the balance/authorization logic.

  • contract.rs

This file is the main contract implementation

use crate::admin::{has_administrator, read_administrator, write_administrator};
use crate::allowance::{read_allowance, spend_allowance, write_allowance};
use crate::balance::{is_authorized, write_authorization};
use crate::balance::{read_balance, receive_balance, spend_balance};
use crate::event;
use crate::metadata::{
    read_decimal, read_name, read_symbol, write_decimal, write_name, write_symbol,
};
use soroban_sdk::{contractimpl, Address, Bytes, Env};

pub trait TokenTrait {
    fn initialize(e: Env, admin: Address, decimal: u32, name: Bytes, symbol: Bytes);

    fn allowance(e: Env, from: Address, spender: Address) -> i128;

    fn increase_allowance(e: Env, from: Address, spender: Address, amount: i128);

    fn decrease_allowance(e: Env, from: Address, spender: Address, amount: i128);

    fn balance(e: Env, id: Address) -> i128;

    fn spendable_balance(e: Env, id: Address) -> i128;

    fn authorized(e: Env, id: Address) -> bool;

    fn transfer(e: Env, from: Address, to: Address, amount: i128);

    fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128);

    fn burn(e: Env, from: Address, amount: i128);

    fn burn_from(e: Env, spender: Address, from: Address, amount: i128);

    fn clawback(e: Env, from: Address, amount: i128);

    fn set_authorized(e: Env, id: Address, authorize: bool);

    fn mint(e: Env, to: Address, amount: i128);

    fn set_admin(e: Env, new_admin: Address);

    fn decimals(e: Env) -> u32;

    fn name(e: Env) -> Bytes;

    fn symbol(e: Env) -> Bytes;
}

fn check_nonnegative_amount(amount: i128) {
    if amount < 0 {
        panic!("negative amount is not allowed: {}", amount)
    }
}

pub struct Token;

#[contractimpl]
impl TokenTrait for Token {
    fn initialize(e: Env, admin: Address, decimal: u32, name: Bytes, symbol: Bytes) {
        if has_administrator(&e) {
            panic!("already initialized")
        }
        write_administrator(&e, &admin);

        write_decimal(&e, u8::try_from(decimal).expect("Decimal must fit in a u8"));
        write_name(&e, name);
        write_symbol(&e, symbol);
    }

    fn allowance(e: Env, from: Address, spender: Address) -> i128 {
        read_allowance(&e, from, spender)
    }

    fn increase_allowance(e: Env, from: Address, spender: Address, amount: i128) {
        from.require_auth();

        check_nonnegative_amount(amount);

        let allowance = read_allowance(&e, from.clone(), spender.clone());
        let new_allowance = allowance
            .checked_add(amount)
            .expect("Updated allowance doesn't fit in an i128");

        write_allowance(&e, from.clone(), spender.clone(), new_allowance);
        event::increase_allowance(&e, from, spender, amount);
    }

    fn decrease_allowance(e: Env, from: Address, spender: Address, amount: i128) {
        from.require_auth();

        check_nonnegative_amount(amount);

        let allowance = read_allowance(&e, from.clone(), spender.clone());
        if amount >= allowance {
            write_allowance(&e, from.clone(), spender.clone(), 0);
        } else {
            write_allowance(&e, from.clone(), spender.clone(), allowance - amount);
        }
        event::decrease_allowance(&e, from, spender, amount);
    }

    fn balance(e: Env, id: Address) -> i128 {
        read_balance(&e, id)
    }

    fn spendable_balance(e: Env, id: Address) -> i128 {
        read_balance(&e, id)
    }

    fn authorized(e: Env, id: Address) -> bool {
        is_authorized(&e, id)
    }

    fn transfer(e: Env, from: Address, to: Address, amount: i128) {
        from.require_auth();

        check_nonnegative_amount(amount);
        spend_balance(&e, from.clone(), amount);
        receive_balance(&e, to.clone(), amount);
        event::transfer(&e, from, to, amount);
    }

    fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) {
        spender.require_auth();

        check_nonnegative_amount(amount);
        spend_allowance(&e, from.clone(), spender, amount);
        spend_balance(&e, from.clone(), amount);
        receive_balance(&e, to.clone(), amount);
        event::transfer(&e, from, to, amount)
    }

    fn burn(e: Env, from: Address, amount: i128) {
        from.require_auth();

        check_nonnegative_amount(amount);
        spend_balance(&e, from.clone(), amount);
        event::burn(&e, from, amount);
    }

    fn burn_from(e: Env, spender: Address, from: Address, amount: i128) {
        spender.require_auth();

        check_nonnegative_amount(amount);
        spend_allowance(&e, from.clone(), spender, amount);
        spend_balance(&e, from.clone(), amount);
        event::burn(&e, from, amount)
    }

    fn clawback(e: Env, from: Address, amount: i128) {
        check_nonnegative_amount(amount);
        let admin = read_administrator(&e);
        admin.require_auth();
        spend_balance(&e, from.clone(), amount);
        event::clawback(&e, admin, from, amount);
    }

    fn set_authorized(e: Env, id: Address, authorize: bool) {
        let admin = read_administrator(&e);
        admin.require_auth();
        write_authorization(&e, id.clone(), authorize);
        event::set_authorized(&e, admin, id, authorize);
    }

    fn mint(e: Env, to: Address, amount: i128) {
        check_nonnegative_amount(amount);
        let admin = read_administrator(&e);
        admin.require_auth();
        receive_balance(&e, to.clone(), amount);
        event::mint(&e, admin, to, amount);
    }

    fn set_admin(e: Env, new_admin: Address) {
        let admin = read_administrator(&e);
        admin.require_auth();
        write_administrator(&e, &new_admin);
        event::set_admin(&e, admin, new_admin);
    }

    fn decimals(e: Env) -> u32 {
        read_decimal(&e)
    }

    fn name(e: Env) -> Bytes {
        read_name(&e)
    }

    fn symbol(e: Env) -> Bytes {
        read_symbol(&e)
    }
}
Enter fullscreen mode Exit fullscreen mode

Our main contract code imports functions from the admin, allowance, balance, and metadata modules.Defines a Token trait with functions for all the key token operations (initialize, transfer, mint, etc.)Defines a check_nonnegative_amount helper function to check that an amount is non-negative (and panic if not). Implements the Token trait for a Token struct, calling into the imported module functions to implement the logic for each token operation.

So in summary, this contract.rs file ties together the logic from the other modules to implement the full Token trait, thereby defining the core functionality of the token contract.

This contract contains several function, which are :

  1. initialize - Sets up the initial state of the contract (admin, decimals, name, symbol). This function needs the following arguments (in the correct respective types) supplied when invoking it:
    admin: Address - The admin address
    decimal: u32 - The number of decimals
    name: Bytes - The token name
    symbol: Bytes - The token symbol

  2. allowance - Reads the allowance for a spender on behalf of a from address. This function needs the following arguments (in the correct respective types) supplied when invoking it:
    from: Address - The address allowing access
    spender: Address - The address allowed to spend

Returns:
i128 - The allowance amount

  1. increase_allowance / decrease_allowance - Increases or decreases an allowance. This function needs the following arguments (in the correct respective types) supplied when invoking it: from: Address - The address allowing access spender: Address - The address allowed to spend amount: i28 - The amount to increase or decrease

Returns:
i128 - The balance amount

  1. balance / spendable_balance - Reads a balance and spendable balance. This function needs the following arguments (in the correct respective types) supplied when invoking it: id: Address - The address to read balance of

Returns:
i128 - The balance amount

  1. authorized - Checks if an address is authorized.This function needs the following arguments (in the correct respective types) supplied when invoking it: id: Address - The address to check authorization of

Returns:
bool - The authorization status (true or false)

  1. transfer / transfer_from - Transfers tokens.This function needs the following arguments (in the correct respective types) supplied when invoking it: For transfer : from: Address - The sender address to: Address - The recipient address amount: i128 - The amount to transfer

For transfer_from:
spender: Address - The address allowed to spend from another address
from: Address - The address the spender is spending from
to: Address - The recipient address
amount: i128 - The amount to transfer

  1. burn / burn_from - Burns (reduces supply of) tokens.This function needs the following arguments (in the correct respective types) supplied when invoking it: For burn: from: Address - The address to burn tokens from amount: i128 - The amount of tokens to burn

For burn_from:
spender: Address - The address allowed to burn from another address
from: Address - The address the spender is burning from
amount: i128 - The amount of tokens to burn

  1. clawback - Clawbacks tokens from an address.This function needs the following arguments (in the correct respective types) supplied when invoking it:
    admin: Address - The admin address (required to call this function)
    from: Address - The address to clawback tokens from
    amount: i128 - The amount of tokens to clawback

  2. set_authorized - Sets the authorization status of an address.This function needs the following arguments (in the correct respective types) supplied when invoking it:
    admin: Address - The admin address (required to call this function)
    id: Address - The address to set authorization status for
    authorize: bool - The new authorization status (true or false)

  3. mint - Mints tokens to an address.This function needs the following arguments (in the correct respective types) supplied when invoking it:
    admin: Address - The admin address (required to call this function)
    to: Address - The recipient address
    amount: i128 - The amount of tokens to mint

  4. set_admin - Sets the admin address.This function needs the following arguments (in the correct respective types) supplied when invoking it:
    admin: Address - The current admin address (required to call this function)
    `new_admin: Address - The new admin address to set

  5. decimals / name / symbol - Reads metadata. These function will return metadata of each function, that are already set when initialize function invoked.

Top comments (0)