Soroban Contracts 101 : Tokens (Part 1)
Tags: soroban, stellar, smartcontract, sorobanathon
Published: 2023-03-20 18:25:46

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](https://github.com/yuzurush/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)

```rust
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);
}
```
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)

```rust
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);
}
```
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)

```rust
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);
}
```

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

```rust
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)
    }
}
```
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

3. `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
4. `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

5. `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)

6. `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


7. `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

8. `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

9. `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)

10. `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

11. `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

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