My first experience with MOVE Smart Contracts

My first experience with MOVE Smart Contracts

I recently tried a new smart contract language, MOVE.

The language is inspired by Rust, so it's not intuitive for Solidity devs.

However, it's super fun to build with.

In this article series, I aim to provide an introduction to the Move language and its features.

Let's dive right in.

Quick History

MOVE is a programming language originally developed by Facebook (now Meta) as part of the Libra (later Diem) blockchain project.

After Diem’s shutdown, MOVE found a new life in blockchain ecosystems like Aptos and Sui. It is designed to be a secure, flexible, and resource-oriented language specifically for blockchain use cases.

Currently, we have different variants for Move, like Aptos Move and Sui Move.

💡
SUI-Move could be substantialy different than original Move to support Sui's object-centric architecture and parallel transaction processing. Terms like dynamic fields and object-first storage are fundamental changes in SUI-MOVE and it appears to be more optimized for asset-centric, high-throughput and object-first executions.

In this article, we are deciphering specifically the Sui Move.

About Move

Move is a compiled, strictly typed language. Strict typing means that every variable, struct, and function must have its type explicitly known at compile time.

No implicit coercion, no duck typing. Although Move is strictly typed, it does allow generic types through the use of type parameters, enabling some degree of flexibility while maintaining strict typing.

It is also heavily inspired by Rust. Concepts like ownership system, borrow checker, move semantics, memory safety without garbage collector, etc.

Terminologies to keep in mind:

Term Meaning
Package A unit of deployment; a collection of modules published together on-chain.
Module A compilation unit inside a package; defines structs, functions, and consts.
Struct User-defined type that can have fields and abilities; may represent objects or in-memory values.
Entry Function Special public function callable by users in transactions; has strict parameter rules.
Abilities Compile-time tags (copy, drop, store, key) controlling how types behave.
Object A struct with key ability and id: UID; persisted on-chain and tracked by Sui.
UID Special built-in type representing the identity of an object. Required for key structs.
Move Semantics Default behavior where values are moved (consumed) on assignment instead of copied.
Reference (&, &mut) A borrow of a value — immutable (&) or mutable (&mut) — enforcing strict aliasing.
Type Abilities copy, drop, store, key – enable value copying, destruction, storage, and object ID usage.

Packages

Packages are a collection of one or more modules that are published on-chain. A single package can contain one or more modules. Modules are the building blocks for packages.

Interestingly, a module or package doesn’t handle storage, so we can’t just define different strings or integers and expect them to be stored on-chain.

Instead, packages and modules only handle HOW data (objects) should behave.

💡
Although we can define a constant, immutable value, they are directly embedded into the bytecode. These can only be specified for primitive types and can only be used as read-only constants. 

Packages, when published, become immutable objects and are assigned a unique ID, like a smart contract address in Ethereum.

Package objects can be upgraded, but that’s out of scope for this article.

Since storage isn't handled by packages, we have an imperative character in Move that deals with storage, i.e., Objects.

Packages define logic and structure, while objects hold data and state.

We will learn about objects later in this article.

Data types

Before we look into objects, we should have an idea of all data types in Sui Move.

Here are some brief details about data types.

There are three ways we can access the types provided by Sui-Move.

The built-in types:

    1. The first one is the built-in types in the Move language, which include bool, unsigned integers (u8, u64 , etc.), and address .
    2. vector<T>: This is simply a collection of multiple values, basically an array. It is the only primitive collection type in Move. The interesting part about vectors is that the compiler only supports their declaration by default. This means, to use methods like push_back, length, pop_back , etc. for a vector, we need the vector module from the standard library.
Note: While the vector itself is a primitive, other collection-like functionalities, such as Maps or Sets, are not built into the language as primitives. Instead, they are implemented as libraries within the Move standard library.

Sui Framework:

    1. The Sui framework on the other hand, provides Sui-specific types like UID, ID, TxContext, and operations related to objects, package upgrades, etc. It also contains the standard library as a dependency. This framework, by default, gets added in our project’s move.toml dependency.

Standard library:

    1. The standard library provides us with types like string, BitVector, Option<T> etc. Additionally, it also provides so many functionalities to work with all the types, including the built-in types.

Both the Sui framework and the Standard Library have some implicit imports. This indicates that we don’t need to explicitly import certain things, like std::option, std::vector from the standard library, and tx_context, object, transfer, etc., from the Sui framework.

User-Defined Types

We also have user-defined types such as Structs and Enums - ( familiar terms for solidity devs ).

Structs:

  • These are a fixed container with named fields — every instance has the same fields and structure, like a User with name, age, and id. In Sui Move, a struct needs the key ability and a field of type UID to be considered an object. The store ability is not strictly required for being an object but is necessary if the object is stored or moved. More on the abilities later.
  • For example:
public struct StructExample has key, store, copy, drop {
        value: u64,
    }

Enums,

  • Enums, on the other hand, are flexible types that can be one of several variants, like a Result that is either Ok(value) or Err(message). You choose one variant when creating it. Enums are useful for expressing options or outcomes, but they can't be used as Sui objects.
  • Enums can have empty values, only values, or named values. For Example:
/// Defines various string segments.
public enum Segment has copy, drop {
    /// Empty variant, no value.
    Empty,
    /// Variant with a value (positional style).
    String(String),
    /// Variant with named fields.
    Special {
        content: vector<u8>,
        encoding: u8, // Encoding tag.
    },
}

/// To instantiate 

/// Constructs an `Empty` segment.
public fun new_empty(): Segment { Segment::Empty }

/// Constructs a `String` segment with the `str` value.
public fun new_string(str: String): Segment { Segment::String(str) }

/// Constructs a `Special` segment with the `content` and `encoding` values.
public fun new_special(content: vector<u8>, encoding: u8): Segment {
    Segment::Special {
        content,
        encoding,
    }
}

If some of this feels unfamiliar, don’t worry — I will break it down later.

Abilities

Move types can have 4 abilities assigned to them.

In simpler words, abilities in move, decide the actions that a particular type is allowed to perform.

  1. copy
  2. drop
  3. store
  4. key

Let's briefly touch on each one of them.

copy

  • Allows copying the value to memory.
Everything inside a sui function remains in memory unless written to storage. There’s no concept of calldata, unlike solidity. We don’t choose between storage or memory in Sui Move.
  • For example, without the copy ability, we won’t be able to do this 👇🏾
let student = StudentID { ... };
let a = student;
let b = student; // Error: already moved

drop

  • Allows values of types with this ability to be popped/dropped. Types with drop can be ignored or destroyed.
  • For example:
module example::drop_demo;

public struct TempData has drop {
    value: u64,
}

public fun make_and_discard() {
    let _ = TempData { value: 42 };
    // No error — TempData has `drop`, so it can be ignored
}

store

  • Allows values of types with this ability to exist inside a value in storage.
  • For Sui, the store determines whether a value can exist in global storage. This is important when dealing with transferring or returning objects from functions.
  • For instance, when you create an object and return it from a function, and then assign that object to an account. This object is being transferred outside of the scope of the module. At this point you will need store.
  • In simpler terms, any struct that is expected to leave the function scope — either by being returned, transferred, or embedded inside another stored value — must have the store ability.

Let’s consider an example to understand this clearly:

  1. SimpleData has store ability because we are embedding it inside an object (DataHolder with key ability).
  2. In the 2nd case, the Outer struct has store, which means all of its fields should have store, hence inner struct also needs to have the store ability.
 /// A struct with store ability
    public struct SimpleData has store {
        value: u64,
    }

    /// An object that contains SimpleData
    public struct DataHolder has key {
        id: object::UID,
        data: SimpleData, // <- Since SimpleData is stored inside an object, it must have `store` ability
    }
    
 /// Similar case 
    public struct Inner has store {
        value: u64,
    }

    public struct Outer has store {
        nested: Inner,
    }
  • When you return a struct from a public entry fun, you are handing it to the user account (across module boundary), a.k.a, needs store ability.
   // Struct needs store if returned out of module
    public struct ReturnableData has store {
        value: u64,
    }

    public entry fun create(): ReturnableData {
        ReturnableData { value: 10 }
    }

key

  • Key is used to specify a struct as an object. It tells that this particular struct should be treated as an object, is stored on-chain independently (top-level), has a unique ID (UID), and is tracked by the Sui runtime for ownership, versioning, etc
  • All fields inside a struct should have the store, but not necessarily the key ability.
  • The built-in types like u64 or string already have copydrop, and store abilities by default, that’s why we don’t explicitly write anything. But any struct that is supposed to be inside another struct with key should have store ability.
💡
A primitive types have all four abilities mentioned here by default , so we don’t explicitly mention them. b. The structs/enums however need explicit mention of abilities by the developer.

To write reusable, flexible modules, we often need type parameters.

Let’s talk about how Move lets us do this using generics and phantom types.

Advanced types - Generics and Phantom

There’s one more interesting feature to learn before we jump into the code, i.e., “Generics”.

You’ll see the use of Generics inside the move package we will talk about in the next part. Don’t get confused if you see terms like “Type Parameters” or “Type Arguments”. These are other names for Generics.

Think of Generics as placeholders for an unknown type. Means you can define a function using Generics, and as the name suggests, that becomes a generic function that will work with any type.

Or you can define a Generic struct, meaning that the struct can have any type you assign. Let’s understand with a small example.

Generic Struct

This defines a Container that can hold any type T.

public struct Container<T> has drop {
    value: T,
}

Generic Function

This creates a new Container<T> from a value of type T.

  • <T> is a placeholder for some type (e.g., u64, String, etc.).
  • When used, T gets replaced with the actual type you pass in.
public fun new<T>(value: T): Container<T> {
    Container { value }
}

The good thing about Generics is that they can be left unused. Here’s an example usage of the above-mentioned function and struct.

let c1 = new(10);               // inferred as Container<u8>
let c2 = new<u64>(100);         // explicitly typed
let c3: Container<String> = new("hi".to_string());

Notice we could pass u8, u64, as well as a string to the same function and struct.

Phantom Types

Sometimes, you want to tag a struct with a type for identification or logic, without actually storing any data of that type. That’s where phantom types come in.

Here’s an example from the Coin system:

public struct TreasuryCap<phantom T> has key, store {
    id: UID,
    total_supply: Supply<T>,
}
  • phantom T tells the compiler: “I’m tracking type T, but I don’t store a value of that type.”
  • It’s used to bind logic to a coin type (T) without carrying it around in memory.
  • Move uses this to enforce type-safe minting: A TreasuryCap<USDC> can’t mint DAI by mistake.

Now that we know about types and abilities, it’s time for Objects.

Objects - The First Class Citizens

In Sui, Objects are first-class citizens. Unlike the traditional terminologies like accounts or balances, SUI uses objects to store anything and everything.

Objects have:

  • A unique ObjectID - Unique identifier for the object derived from the transaction context.
  • A version - The number of times the object has been mutated or written to the blockchain.
  • An owner (account address, shared, or another object) - we will talk about it in a bit.

We define objects with a struct type. The catch is, this struct should have the key ability, and id: UID as the first field.

Example:

public struct MyCounter has key {
    id: UID,
    value: u64,
}

In the above example:

  • MyCounter is a custom Object type.
  • id: UID gives it a unique identity.
  • value: u64 is the data we are storing.

Ownership of objects

Expanding more on the ownership of objects. Every object has owners, it’s an in-built feature of the Sui chain. Let’s discuss the three types:

  1. Owned by an address:
    1. Simply, an account address is the owner. This indicates that only the owner can pass this object in an entry function and mutate it.
    2. For example, you own 100 USDT on the Sui chain, which means you own one or more objects of USDT coin combined together, and all these objects have your account address as the owner.
    3. It means, there can be one object of USDT with a balance of 100, or there can be 10 objects of USDT coin with a balance of 10, and so on. The interesting part is, you can merge, split, or individually transfer these objects.
  2. Shared Object:
    1. A shared object is not owned by anyone — anyone can read or write to it, following the access logic we define.
    2. In simpler terms, a shared object is not necessarily "unowned" but is instead accessible to multiple users concurrently.
    3. Ownership in this context refers to shared ownership, where access control and mutability are defined by the object’s shared nature. The developer can put accessibility checks based on the logic, for example a whitelist etc.
    4. This also brings us to a another important point. When a Shared Object is altered, it needs the consensus execution, because they can't be parallelly executed. Owned objects, on the other hand, don't mess with other objects, so they can be executed parallelly, without needing the consensus.
  3. Owned by another object ( composability like NFTs with metadata):
    1. One object can own another object. This can be used for nested compositions, building rich hierarchies.
    2. For example, imagine a conference pass system — you hold a master pass object, and based on your ticket tier, it owns several sub-event passes (also objects). These child passes can’t exist independently — they are tied to the parent object.
  4. Frozen State: The objects can also be immutable, meaning they are permanently frozen, and can’t be moved/mutated anymore.

So far, we’ve talked about types, objects, and abilities, etc. To make everything work together, we need the logic of execution, i.e. functions.

Let’s look at how functions work and how they drive logic in a Move module.

Functions and Parameters

Functions are the building blocks of any Sui Move module. Every operation — creating objects, updating state, transferring ownership, minting tokens — is done inside a function.

A function is declared using the fun keyword:

fun add(x: u64, y: u64): u64 

{
    x + y
}

Function Visibility

Keyword Who can call it? Used for
fun Only inside the same module Internal logic
public fun Any module On-chain module-to-module calls
public entry fun End users (wallets, frontend, CLI) Transaction entry points
public(package) fun Any module within the same package Internal APIs across package modules

Function Syntax

<visibility>? <entry>? fun <name><type_params>(<params>): <return_type> {
    // body
}
  • visibility: optional (public, public(package))
  • entry: optional (entry only for functions callable by end users)
  • <type_params>: optional generic type params like <T>
  • params: comma-separated parameters
  • return_type: single type, tuple, or () for nothing

Example with All Features

public entry fun transfer_coin<T>(
    coin: Coin<T>,
    recipient: address,
    ctx: &mut TxContext
) {
    transfer::public_transfer(coin, recipient);
}
  • entry → can be called in a transaction
  • public → visible from outside the module
  • T → generic type parameter
  • ctx → always required for any state-changing function

Parameters explained

&T Immutable reference to a value — cannot modify it.
&mut T Mutable reference — allows changing the passed object.
T (by value) Valid only for primitives or small structs with copy/drop abilities.
vector<T> Can be passed directly if T is a valid type (e.g., vector<u64>).
TxContext Must be &mut TxContext, always passed last. Required for state changes.

Entry Function Rules

  • Must accept only certain parameter types:
    • Object references
    • Pure values (u64, bool, address, etc.)
    • TxContext
  • The last parameter must be &mut TxContext

Valid:

public entry fun create(ctx: &mut TxContext) { ... }

Invalid:

public entry fun bad(ctx: TxContext) { ... } // not a reference

Multiple Return Values

Functions can return tuples:

fun get_name_and_age(): (string::String, u8) {
    (string::utf8(b"John"), 25)
}

let (name, age) = get_name_and_age();

Wrapping it UP

Alright, so we covered a bunch of details in this article.

We learned how Move offers a unique and powerful approach to building blockchain applications, combining the robustness of Rust-inspired features with a blockchain-specific focus on resource management and ownership.

One of the standout features of Sui-Move is its emphasis on object-centric storage, where objects are first-class citizens. This paradigm shift from traditional account-based models makes it possible to handle assets and data more dynamically. Additionally, the strict typing system and abilities like copy, drop, store, and key ensure that developers maintain fine-grained control over data integrity and safety.

Additionally, by leveraging generics and phantom types, developers can write flexible and reusable modules, further enhancing the efficiency of smart contract development. The careful balance between flexibility and safety makes Sui Move particularly suited for building next-generation blockchain applications.

With a solid foundation in these concepts, you can confidently explore more advanced topics, such as object ownership and multi-version concurrency control.

My journey into Sui Move has just begun, but the fundamentals covered so far have already revealed the language's immense potential for building scalable, secure, and high-performance blockchain solutions.

See you all in the next part of this article series.


Join Decipher Club today

Simplifying Web3 and Technology for everyone

Subscribe Now