DEV Community

yuzurush
yuzurush

Posted on • Updated on

Soroban Contracts 101 : Tokens (Part 2)

  • events.rs

Contains event emitting logic


use soroban_sdk::{Address, Env, Symbol};

pub(crate) fn increase_allowance(e: &Env, from: Address, to: Address, amount: i128) {
    let topics = (Symbol::new(e, "increase_allowance"), from, to);
    e.events().publish(topics, amount);
}

pub(crate) fn decrease_allowance(e: &Env, from: Address, to: Address, amount: i128) {
    let topics = (Symbol::new(e, "decrease_allowance"), from, to);
    e.events().publish(topics, amount);
}

pub(crate) fn transfer(e: &Env, from: Address, to: Address, amount: i128) {
    let topics = (Symbol::short("transfer"), from, to);
    e.events().publish(topics, amount);
}

pub(crate) fn mint(e: &Env, admin: Address, to: Address, amount: i128) {
    let topics = (Symbol::short("mint"), admin, to);
    e.events().publish(topics, amount);
}

pub(crate) fn clawback(e: &Env, admin: Address, from: Address, amount: i128) {
    let topics = (Symbol::short("clawback"), admin, from);
    e.events().publish(topics, amount);
}

pub(crate) fn set_authorized(e: &Env, admin: Address, id: Address, authorize: bool) {
    let topics = (Symbol::new(e, "set_authorized"), admin, id);
    e.events().publish(topics, authorize);
}

pub(crate) fn set_admin(e: &Env, admin: Address, new_admin: Address) {
    let topics = (Symbol::short("set_admin"), admin);
    e.events().publish(topics, new_admin);
}

pub(crate) fn burn(e: &Env, from: Address, amount: i128) {
    let topics = (Symbol::short("burn"), from);
    e.events().publish(topics, amount);
}

Enter fullscreen mode Exit fullscreen mode

This module contains implementations of functions that emit events from the token contract. Events allow light clients to track state changes on the blockchain without needing to fully verify all transactions.

The functions are:

increase_allowance - Emits an event when an allowance is increased
decrease_allowance - Emits an event when an allowance is decreased
transfer - Emits an event when tokens are transferred
mint - Emits an event when tokens are minted
clawback - Emits an event when tokens are clawed back
set_authorized - Emits an event when an account's authorization is changed
set_admin - Emits an event when the admin is changed
burn - Emits an event when tokens are burned

Each function requires various arguments (Env, addresses, amounts, authorize flag) and publishes an event using the Env e argument, specifying topics ( identifiers for the event type and relevant addresses/admin) and data (the amount or authorize flag).

  • lib.rs

This is our main crate file

#![no_std]

mod admin;
mod allowance;
mod balance;
mod contract;
mod event;
mod metadata;
mod storage_types;
mod test;

pub use crate::contract::TokenClient;
Enter fullscreen mode Exit fullscreen mode

This file imports and exports the key modules (functionality groupings) of the token crate, making the main TokenClient type available for use by other crates.

  • metadata.rs

Contains metadata logic (reading/writing name, symbol, decimals)

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

pub fn read_decimal(e: &Env) -> u32 {
    let key = DataKey::Decimals;
    e.storage().get_unchecked(&key).unwrap()
}

pub fn write_decimal(e: &Env, d: u8) {
    let key = DataKey::Decimals;
    e.storage().set(&key, &u32::from(d))
}

pub fn read_name(e: &Env) -> Bytes {
    let key = DataKey::Name;
    e.storage().get_unchecked(&key).unwrap()
}

pub fn write_name(e: &Env, d: Bytes) {
    let key = DataKey::Name;
    e.storage().set(&key, &d)
}

pub fn read_symbol(e: &Env) -> Bytes {
    let key = DataKey::Symbol;
    e.storage().get_unchecked(&key).unwrap()
}

pub fn write_symbol(e: &Env, d: Bytes) {
    let key = DataKey::Symbol;
    e.storage().set(&key, &d)
}
Enter fullscreen mode Exit fullscreen mode

The metadata.rs file contains functions for reading and writing metadata from storage.

It imports:

DataKey from storage_types (to access keys for storage)
Env, Bytes from soroban_sdk (to access the environment and work with byte vectors)

It then defines functions for:

Reading/writing the decimal places
Reading/writing the token name
Reading/writing the token symbol

Each function takes an Env reference and either reads a value from or writes a value to storage, using the appropriate DataKey and calling getter/setter functions on the Env's storage.
So in summary, this file provides an interface for managing the metadata (decimals, name, symbol) stored for the token contract.

  • storage_types.rs

Types for contract storage

use soroban_sdk::{contracttype, Address};

#[derive(Clone)]
#[contracttype]
pub struct AllowanceDataKey {
    pub from: Address,
    pub spender: Address,
}

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Allowance(AllowanceDataKey),
    Balance(Address),
    Nonce(Address),
    State(Address),
    Admin,
    Decimals,
    Name,
    Symbol,
}
Enter fullscreen mode Exit fullscreen mode

The storage_types.rs file defines types for keys used to access storage.

It imports Address from soroban_sdk

It defines:

AllowanceDataKey - A key for allowance data, containing from and spender addresses
DataKey - An enum containing keys for all the main storage items:

  • Allowance (using AllowanceDataKey)
  • Balance (using an address)
  • Nonce (using an address)
  • State (using an address)
  • Admin
  • Decimals
  • Name
  • Symbol

The #[contracttype] attribute indicates these are types used for a Soroban contract.
So in summary, this file defines typed keys to use to access the key storage items for the token contract, allowing strong typing and safety when accessing storage.

Running Contract Test

To ensure that the contract functions as intended, you can run the contract tests using the following command:

cargo test 
Enter fullscreen mode Exit fullscreen mode

If the tests are successful, you should see an output similar to:

running 8 tests
test test::decimal_is_over_max - should panic ... ok
test test::initialize_already_initialized - should panic ... ok
test test::xfer_insufficient_balance - should panic ... ok
test test::test_burn ... ok
test test::xfer_from_insufficient_allowance - should panic ... ok
test test::xfer_spend_deauthorized - should panic ... ok
test test::xfer_receive_deauthorized - should panic ... ok
test test::test ... ok

test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.37s
Enter fullscreen mode Exit fullscreen mode

Building The Contract

To build the contract, use the following command:

cargo build --target wasm32-unknown-unknown --release
Enter fullscreen mode Exit fullscreen mode

This should output a .wasm file in the ../target directory:

../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm
Enter fullscreen mode Exit fullscreen mode

Deploying The Contract

To deploy the contract, use the following command:

soroban contract deploy --wasm /target/wasm32-unknown-unknown/release/soroban_token_contract.wasm 
Enter fullscreen mode Exit fullscreen mode

Invoking The Contract

This token contract contains many functions, but in this post I will only demonstrate initializing the token and minting tokens to an address. You can explore the other functions yourself. To initialize the token i'm using this command :

$ soroban contract invoke --id 31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e --secret-key SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG -- initialize --admin GAREW2RCA4GELVLKPAIZNMNETTN57PAIVGOMPNMXHV3KMHLMMTPT2BAD --decimal 100 --name "4d79546f6b656e" --symbol "4d544b"
success
Enter fullscreen mode Exit fullscreen mode
  • 31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e
    • The ContractID of the deployed contract before
  • SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG - Secret Key from the account used to invoke the function
  • initialize - invoked function
  • GAREW2RCA4GELVLKPAIZNMNETTN57PAIVGOMPNMXHV3KMHLMMTPT2BAD - - The admin address
  • 100 - value for decimal
  • 4d79546f6b656e - The token name (in Hex bytes - "MyToken" in string)
  • 4d544b - The Token Symbol (in Hex bytes - "MTK" in string)

Token Contract already initialized, then i will mint that token into an address, using this command :

$ soroban contract invoke --id 31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e --secret-key SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG -- mint --admin GAREW2RCA4GELVLKPAIZNMNETTN57PAIVGOMPNMXHV3KMHLMMTPT2BAD --to GDB3PKTGIMFAE4D36RAU5CKV3BHS75G5PMTJECIWAGELUSVBEMUBOUA3 --amount 100
success
Enter fullscreen mode Exit fullscreen mode
  • 31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e - The Contract ID
  • SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG - The Secret Key from the account used to invoke the function
  • mint - The Invoked Function
  • GAREW2RCA4GELVLKPAIZNMNETTN57PAIVGOMPNMXHV3KMHLMMTPT2BAD - The Admin Address of The Token Contract
  • GDB3PKTGIMFAE4D36RAU5CKV3BHS75G5PMTJECIWAGELUSVBEMUBOUA3 - The recipient address
  • 100 - Token minted amount

To make sure the token already minted we will check it using this command :

$ soroban contract invoke --id 31e2cf6483b1d6e4149780c7b97a455f3d17d6b43ef9050549fbd987d988039e --secret-key SCTDJOSFJOD3XGDAPDW73TSEVKRHSYVBQ4CK3QEHIAI4NH6SWDG53EBG -- balance --id GDB3PKTGIMFAE4D36RAU5CKV3BHS75G5PMTJECIWAGELUSVBEMUBOUA3
success
"100"
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it! We explored all modules code and the key parts of a token contract on Soroban. We saw how the contract is initialized and token minted. By piecing together the admin, allowance, balance, event, metadata, and storage_types modules, and using the TokenClient interface, a custom token contract can be easily developed and managed on Soroban. Stay tuned for more post in this "Soroban Contracts 101" Series where we will dive deeper into Soroban Contracts and their functionalities.

Top comments (1)

Collapse
 
fyodorio profile image
Fyodor

What about non-financial assets? Do you have some insights on working with them? Like, for instance, storing some chunks of structural data as an asset? (sorry if it's a dumb question 😅)