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);
}
This module imports DataKey
from storage_types.rs
and types
from soroban_sdk
.
It defines four admin-related functions:
-
has_administrator
- Checks if an admin is set. It uses aDataKey
of typeAdmin
and the storagehas
method to check. - read_administrator - Reads the admin address. It uses a
DataKey
of typeAdmin
and the storageget_unchecked
method (panicking if no admin is set). -
write_administrator
- Writes an admin address. It uses aDataKey
of typeAdmin
and the storageset
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);
}
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:
-
read_allowance
- Reads the allowance for aspender
on behalf of afrom
address. It uses aDataKey
of typeAllowanceDataKey
to look up the data in contract storage, and returns 0 if no allowance is set. -
write_allowance
- Writes an allowance amount for aspender
on behalf of afrom
address. It uses aDataKey
of typeAllowanceDataKey
to store the data. -
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);
}
It imports DataKey
from storage_types.rs
and types
from soroban_sdk
.
It defines six balance-related functions:
-
read_balance
- Reads the balance for an address. It uses aDataKey
of typeBalance
and the storageget
method, returning 0 if no balance is set. -
write_balance
- Writes a balance for an address. It uses aDataKey
of typeBalance
and the storageset
method. -
receive_balance
- Increases the balance for anaddress
by someamount
. It first reads the current balance, checks authorization status, then increases and writes the new balance. It panics if deauthorized. - spend_balance - Decreases the balance for an
address
by someamount
. 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. -
is_authorized
- Checks the authorization status for anaddress
. It uses aDataKey
of typeState
and the storageget
method, defaulting totrue
if no state is set. -
write_authorization
- Writes the authorization status for anaddress
. It uses aDataKey
of typeState
and the storageset
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)
}
}
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 :
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 symbolallowance
- 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
-
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 accessspender: Address
- The address allowed to spendamount: i28
- The amount to increase or decrease
Returns:
i128
- The balance amount
-
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
-
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)
-
transfer
/transfer_from
- Transfers tokens.This function needs the following arguments (in the correct respective types) supplied when invoking it: Fortransfer
:from: Address
- The sender addressto: Address
- The recipient addressamount: 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
-
burn
/burn_from
- Burns (reduces supply of) tokens.This function needs the following arguments (in the correct respective types) supplied when invoking it: Forburn
:from: Address
- The address to burn tokens fromamount: 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
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 clawbackset_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)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 mintset_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 setdecimals
/name
/symbol
- Reads metadata. These function will return metadata of each function, that are already set wheninitialize
function invoked.
Top comments (0)