DEV Community

Nacho Colomina Torregrosa
Nacho Colomina Torregrosa

Posted on

Adding supply control to a soroban token

Introduction

In the realm of tokenization projects, supply refers to the total number of tokens that will ever be minted.
A fixed token supply ensures there will never be more tokens minted than what was initially established. This can be crucial for projects where a predetermined value is assigned to each token. For instance, a project that tokenizes a single, high-value property might leverage a fixed supply to represent ownership. Let's say the property is valued at $1 million, and the project creates 1 million tokens. Each token would then represent $1 of ownership in the property. By setting a limit on the number of tokens, scarcity is introduced, potentially driving up the value of each individual token.

In the next sections, we are going to learn how to incorporate supply capabilities into the soroban token example.

You can see the complete code here

Adding the Supply module

The soroban token example is divided in several modules. This makes the code more readable and easier for other programmers to understand. The main code is located in the contract.rs module which contains the contract implementation and uses the other modules functions.
Let's first add a new variant to the DataKey enum which is located in the storage_types module.

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

Now, let's create a supply module which will use the Supply DataKey variant to create and manage the supply storage key.

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

pub fn read_supply(e: &Env) -> i128 {
    let key = DataKey::Supply;
    e.storage().instance().get(&key).unwrap()
}

pub fn write_supply(e: &Env, amount: &i128) {
    let key = DataKey::Supply;
    e.storage().instance().set(&key, amount);
}

pub fn decrement_supply(e: &Env, amount: &i128) {
    let key = DataKey::Supply;
    let current_supply: i128 = e.storage().instance().get(&key).unwrap();
    let new_supply = current_supply - amount;

    e.storage().instance().set(&key, &new_supply);
}

pub fn increment_supply(e: &Env, amount: &i128) {
    let key = DataKey::Supply;
    let current_supply: i128 = e.storage().instance().get(&key).unwrap();
    let new_supply = current_supply + amount;

    e.storage().instance().set(&key, &new_supply);
}
Enter fullscreen mode Exit fullscreen mode

The supply module includes the following functions:

  • read_supply: Gets the token supply from the storage.
  • write_supply: Sets the token supply on the storage.
  • decrement_supply: Decrements the token supply. This function will be called after minting an address with tokens.
  • increment_supply: Increments the token supply. It will be called after burning tokens so that those tokens are removed from the circulation but they can be minted again.

Finally, we have to add the module to the lib.rs file:

#![no_std]

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

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

Write supply when initialize the token

The initialize function can be executed once and configures the following token parameters:

  • Decimals.
  • Symbol.
  • Token administrator address.

We have to modify this function so that:

  • It receives the supply as parameter.
  • It writes the supply to the storage.
pub fn initialize(e: Env, admin: Address, decimal: u32, name: String, symbol: String, supply: i128) {
    if has_administrator(&e) {
        panic!("already initialized")
    }
    write_administrator(&e, &admin);
    if decimal > u8::MAX.into() {
        panic!("Decimal must fit in a u8");
    }

    if supply <= 0 {
        panic!("Supply must be greater than 0");
    }

    write_supply(&e, &supply);
    write_metadata(
        &e,
        TokenMetadata {
            decimal,
            name,
            symbol,
        },
    )
}
Enter fullscreen mode Exit fullscreen mode

Before writing the supply, the function checks that the supply amount received is greater than 0. If not, the function panics.

Decrement supply after minting

Having the supply module ready, we have to modify the mint function with the following new functionality :

  • Checks whether there is enough supply to mint the address with the specified amount
  • Decrements the available supply after minting an address.
pub fn mint(e: Env, to: Address, amount: i128) {
    check_nonnegative_amount(amount);
    let admin = read_administrator(&e);
    admin.require_auth();

    if amount > read_supply(&e) {
        panic!("Amount greater than remaining supply");
    }

    e.storage()
        .instance()
        .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

    receive_balance(&e, to.clone(), amount);
    decrement_supply(&e, &amount);
    TokenUtils::new(&e).events().mint(admin, to, amount);
}
Enter fullscreen mode Exit fullscreen mode

The mint function uses the receive_supply function from the supply module to get the current supply. If there isn't enough supply the function panics. If there is enough supply, it decrements the supply with the amount minted.

Create a supply function

We need a function in the contract which returns the current supply:

pub fn supply(e: Env) -> i128 {
    e.storage()
        .instance()
        .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

    read_supply(&e)
}
Enter fullscreen mode Exit fullscreen mode

It uses the read_supply function from the supply module to get the current supply.

Increments supply after burning

Every time that an amount of tokens are burned they are removed from the circulation. As we are controlling the supply, we have to increment the supply after burning so those tokens can be minted again in the future. To do it, we have to modify both burn and burn_from functions.

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

   e.storage()
       .instance()
       .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

   spend_balance(&e, from.clone(), amount);
   increment_supply(&e, &amount);
   TokenUtils::new(&e).events().burn(from, amount);
}

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

   e.storage()
       .instance()
       .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

   spend_allowance(&e, from.clone(), spender, amount);
   spend_balance(&e, from.clone(), amount);
   increment_supply(&e, &amount);
   TokenUtils::new(&e).events().burn(from, amount)
}
Enter fullscreen mode Exit fullscreen mode

As shown, after spending the balance the supply is incremented with the amount burned.

Conclusion

In this article, we've learned how to add supply capabilities to a soroban token so that only can be minted a limited number of tokens. This article shows a fixed supply example, that is, once the contract is initialized, the supply cannot be modified.
There could be another use cases where the supply would be dynamic (it could be modified following some rules). In this case, we would add a function to the contract to set the supply. Obviously, only the contract administrator should be able to modify the supply.

Top comments (0)