My First Mini-Project in SUI-Move - Part 2

My First Mini-Project in SUI-Move - Part 2

In my previous article, we got an overview of different components that combine and make a working Move Smart Contract (packages) on the Sui chain.

This article aims to understand how coins work in Sui; more importantly, this will familiarise you with modules, objects, the Sui framework, etc.

We’ll take an example of a coin to understand how it works in practice.

  1. We create a package with just one module.
  2. The init function of this module contains the logic to create a new currency.
  3. We define a mint function to mint new coins.
  4. Publish the package.
  5. And then, Mint our coins.

To develop locally, we need the Sui binary. The installation is quite simple, and you may find it in the move book itself.

Initiating the project

We’ll start by creating a project with the command sui move new Token, where Token is our project directory’s name.

When you initialise a Sui Move project, your folder looks something like this:

Token/
├── Move.toml         # Project manifest
└── sources/
    └── TokenModule.move  # Your actual smart contract module
  • Move.toml contains metadata like package name, dependencies (like sui-framework), address declarations, etc.
  • The sources/ folder contains .move files — each one is a module, and together they form your package.

As previously discussed in part 1 of this article, once you write your code inside this module, you publish the whole package to the chain.

After that, you can start interacting with it using the CLI or other tools.

Let's start writing the Module

At the top, we have module TokenPackage::TokenModule;

This indicates that the TokenModule comes under the package TokenPackage.

The names can be anything, I’ve used Module and Package specifically for the ease of understanding.

Then we write the import declarations for the relevant modules.

In our case, we are importing the coin module as well as the txContext module from the Sui Framework.

use sui::coin::{Self, TreasuryCap};
use sui::tx_context::{sender};

Notice that we import the TreasuryCap struct from coin and the sender from the tx_context.

Now, what's Self ?

Self means the coin module itself is imported, so that we can use its function without needing the full path.

For instance:

coin::mint(...)          // because Self imported the module namespace

Without SelfYou’d need to use the full path

sui::coin::mint(...)

For clarity:

use sui::coin::{Self, TreasuryCap};

Is equivalent to:

use sui::coin as coin;
use sui::coin::TreasuryCap;

Now, coming to the imported modules and their significance.

tx_context

The module tx_context provides us with all the details related to a transaction, like sender, timestamps, digest, etc. In our case, we are importing sender from this module.

You’ll also notice the extensive usage of the TxContext struct, not only in our Token module but in any module, and we are not importing it. The reason is that TxContext is implicitly imported; we don’t need to import it explicitly.

Coin

Sui doesn’t have standards like ERC20, ERC777, etc. Instead, there’s a module called Coin provided by the Sui framework.

This Coin module provides us with everything to create/maintain a currency on the Sui Chain.

This makes development more consistent and developer-friendly, because you don’t need to worry about following a standard out of multiple ones, to make your coin compatible with the ecosystem.

After importing these modules, we need a struct, but this is not a normal struct with fields; it’s a zero-sized struct. This acts as the One Time Witness (OTW). We’ll discuss it in more detail in a while.

Note here that the OTW struct should be named the same as the module name, but in uppercase, as shown below:

/// Define the package.
module TokenPackage::TokenModule;

///import relevant packages 
use sui::coin::{Self, TreasuryCap};
use sui::tx_context::{sender};

/// OTW and the type for the Token.
/// name same as the module name
public struct TOKENMODULE has drop {} 

Now that we have the initial setup, it’s time to write the logic for creating a new coin. We will do this by calling the create_currency function from the Coin module, inside our init function.

The init() function

The Init function here is acting like a constructor in Solidity.

"While Move doesn’t have constructors in the traditional sense, an init function acts as a one-time setup hook when the package is first published. If defined with the correct signature and marked as private, it will be invoked automatically."

However, if you define a function named init that is:

  • marked as private,
  • takes only &mut TxContext (or OTW + ctx) and,
  • returns nothing,

Then Sui will automatically call it once when the package is published, but only during the initial publish, and not upgrades.

In simpler terms, init is not a reserved keyword. If we don’t follow the standard, for example, by marking the function as public or entry, then it won’t get invoked during publishing.

Our init function will look like this:

fun init(otw: TOKENMODULE, ctx: &mut TxContext) {
    let (treasury_cap, metadata) = coin::create_currency(
        otw,
        9,
        b"SMPL",
        b"Simple Token",
        b"Simple Token showcases",
        option::none(),
        ctx,
    );

    transfer::public_freeze_object(metadata);
    transfer::public_transfer(treasury_cap, sender(ctx));
}

create_currency

To create a coin, we need to use the create_currency function from the Coin module. This function takes the following arguments related to the coin we are creating.

  1. OTW ( one time witness)
  2. decimal ( standard value is 9 on Sui chain )
  3. Token symbol
  4. Token Name
  5. Token Description
  6. icon_url ( we can also specify an icon that explorers, DEXes, etc, can fetch)
  7. ctx - The transaction context.

One Time Witness

The TOKENMODULE struct we defined at the top. This is our phantom marker, a zero-sized struct used to tag and identify this coin type uniquely. OTW is not stored on-chain and is only used to identify your coin. PackageID::ModuleName::StructName becomes the unique identity of a coin.

The objects, treasury_cap and metadata use the otw as a phantom type for uniqueness. You will understand this better when we dive more into treasury_cap and metadata objects.

What happens inside create_currency?

When we call the create_currency function. It does two main tasks. The first one is to check the uniqueness. It asserts that our OTW is unique.

Next, it simply creates and returns two objects treasury_cap and metadata. We are then freezing the metadata and transferring the treasury_cap to the sender.

treasury_cap

treasury_cap is a struct that defines who can mint or burn the coin. In our example, we are transferring it to the sender, which means only the sender can mint this token. It also keeps track of the total supply.

public struct TreasuryCap<phantom T> has key, store {
    id: UID,
    total_supply: Supply<T>,
}

This is where otw is used, treasury_cap takes the coin type we passed, in our example, it was the TOKENMODULE struct. Which means this object is now tied to our TOKENMODULE coin type. Whoever is the owner of this particular treasury_cap object has the right to mint/burn TOKENMODULE coin.

metadata

As suggested by the name, this object will store the details of our coin, whatever we passed in the create_currency function.

Here’s the actual struct for metadata 👇🏾

public struct CoinMetadata<phantom T> has key, store {
    id: UID,
    decimals: u8,
    name: string::String,
    symbol: ascii::String,
    description: string::String,
    icon_url: Option<Url>,
}

Keep in mind that this metadata object has nothing to do with the treasury_cap object or minting/burning the coin. This is only used by the external services to get a coin’s details.

Also, notice, this struct has the <phantom T> as well. Which means it is also tied to our coin type, TOKENMODULE.

We use the public_freeze_object function to do so. This function makes an object completely immutable. It can no longer be mutated or transferred. It’s up to the developer to make the metadata immutable or mutable.

Combining it ALL

Now that you know, we have the treasury_cap and metadata objects living on the Sui chain. We should look into how exactly the minting or burning happens.

Before learning about minting, you need to know that for this simple coin example, the treasury_cap object is directly owned by an account. This means all the functions related to it are already public. That explains we don’t need to have an explicitly written mint function in our module, because the owner can just call the mint function on sui framework.

Unlike Ethereum, where the transfer of native ETH differs from ERC20 tokens, in Sui, all coins, including SUI itself, follow the same object model (Coin<T>). They share the same mechanics for transfer, merging, and splitting. The only difference is their unique type identifier (T), not their behaviour.

Minting of coins

For demonstration purposes, we have this mint function in our module:

public entry fun mint(
    c: &mut TreasuryCap<TOKENMODULE>,
    amount: u64,
    recipient: address,
    ctx: &mut TxContext,
) {
    coin::mint_and_transfer(c, amount, recipient, ctx);
}

Nice and simple.

We pass our treasury_cap, the amount of coins to mint, the recipient address, and the Transaction Context. Then we call the mint_and_transfer function on the Coin Module.

mint_and_transfer

Now, looking at the mint_and_transfer function inside the Coin module:

public entry fun mint_and_transfer<T>(
    c: &mut TreasuryCap<T>,
    amount: u64,
    recipient: address,
    ctx: &mut TxContext,
) {
    transfer::public_transfer(mint(c, amount, ctx), recipient)
}

It handles the minting by calling the mint function and then transferring the Coin object it received from the mint function to the recipient. We could also call the mint function directly, which means explicitly handling the transfer.

mint

Let’s look at the mint function:

public fun mint<T>(cap: &mut TreasuryCap<T>, value: u64, ctx: &mut TxContext): Coin<T> {
    Coin {
        id: object::new(ctx),
        balance: cap.total_supply.increase_supply(value),
    }
}

It creates a new Coin object with the value we passed, and returns it, also increasing the total supply.

To clear the confusion you might have, this line cap.total_supply.increase_supply(value) returns the value itself after updating the total supply.

The Coin struct

Now the recipient has the desired Coin object with the desired balance. Let’s quickly look at the Coin struct.

public struct Coin<phantom T> has key, store {
    id: UID,
    balance: Balance<T>,
}

It has an ID, obviously needed because it will be stored on chain as an object. It also stores the balance this particular object has, and it does so by using the Balance struct from the Balance module. This Balance struct also has the phantom type, which means it also binds the token type, which is useful in multi-token protocols. It is better than just using an unsigned integer.

This Coin object is now stored on-chain with the recipient as the owner.

If you remember, I talked about owning 100 USDC in the previous article, where it could be a single object with a balance of 100 or 10 objects with a balance of 10 each, or so on. Now it should be clearer with the explanation above.

To summarise it all.

  • We created a Coin. It means we created two objects, treasury_cap<TOKENMODULE> and metadata<TOKENMODULE>.
  • The owner of treasury_cap<TOKENMODULE> can mint new coins, which means create new ones Coin<TOKENMODULE> objects.
  • The Coin objects reside independently on the Sui chain.

Transferring Coins

The Coin module also comes with some important functions, like split, merge, etc.

You might wonder, how does the transfer of coins work? Well, as coins are not just some unsigned integer values here, unlike solidity. They are objects, which means we will use the same transfer functions we use for any other object.

This raises a question: what if I want to transfer 20 out of my 100 coins, and all 100 coins are in the same object? This is where split comes in. You split the coin object into two objects, keep the object with 80 coins with you, and send the other one with 20 coins to the recipient.

A user doesn’t need to worry about splitting and transferring, the coin module provides a function split_and_transfer that does both tasks.

Complete module

/// Create a simple Token.
module TokenPackage::TokenModule;

use sui::coin::{Self, TreasuryCap, Coin};
use sui::tx_context::{sender};

/// OTW and the type for the Token.
public struct TOKENMODULE has drop {}

fun init(otw: TOKENMODULE, ctx: &mut TxContext) {
    let (treasury_cap, metadata) = coin::create_currency(
        otw,
        9,
        b"SMPL",
        b"Simple Token",
        b"Simple Token showcases",
        option::none(),
        ctx,
    );

    transfer::public_freeze_object(metadata);
    transfer::public_transfer(treasury_cap, sender(ctx));
}


/// Mint `amount` of `Coin` and send it to `recipient`.
public entry fun mint(
    c: &mut TreasuryCap<TOKENMODULE>,
    amount: u64,
    recipient: address,
    ctx: &mut TxContext,
) {
    coin::mint_and_transfer(c, amount, recipient, ctx);
}

#[test_only]
public fun init_for_test(ctx: &mut TxContext) {
    init(TOKENMODULE {}, ctx);
}

s

Summarizing what we learned so far

One of the first things you might notice when working with Sui Move—especially if you come from a Solidity background—is how streamlined and developer-friendly the experience feels.

This is largely because Sui provides many essential features and data structures through its framework and standard library. As a result, developers spend less time reimplementing common functionality and can rely on built-in, audited components, which reduces the likelihood of human error and improves overall security.

Another key strength of Sui is its object-centric model. Unlike account-based systems, Sui treats all assets and data as independent objects with separate storage. This design enables parallel transaction execution, where operations involving unrelated objects can proceed without waiting for consensus. It’s a powerful optimization that sets Sui apart. Of course, shared objects—accessible by multiple users—are an exception, requiring consensus to ensure consistency.

These two articles have laid the foundation for understanding the core principles of Move on the Sui blockchain. We've covered the fundamentals of packages, modules, object storage, and coin creation. But there’s much more to discover. As I continue exploring the ecosystem, I look forward to sharing deeper insights, real-world projects, and advanced patterns in future posts. Stay tuned.

References used

The Move Book - https://move-book.com/index.html

Sui Docs - https://docs.sui.io/