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.
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.
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:
- The first one is the built-in types in the Move language, which include
bool
, unsigned integers (u8
,u64
, etc.), andaddress
. - 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:
- 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’smove.toml
dependency.
Standard library:
- 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 eitherOk(value)
orErr(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.
- copy
- drop
- store
- 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:
SimpleData
hasstore
ability because we are embedding it inside an object (DataHolder
withkey
ability).- In the 2nd case, the Outer struct has
store
, which means all of its fields should havestore
, henceinner
struct also needs to have thestore
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 thekey
ability. - The built-in types like
u64
orstring
already havecopy
,drop
, andstore
abilities by default, that’s why we don’t explicitly write anything. But any struct that is supposed to be inside another struct withkey
should havestore
ability.
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 typeT
, 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 mintDAI
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:
- Owned by an address:
- Simply, an account address is the owner. This indicates that only the owner can pass this object in an entry function and mutate it.
- 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.
- 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.
- Shared Object:
- A shared object is not owned by anyone — anyone can read or write to it, following the access logic we define.
- In simpler terms, a shared object is not necessarily "unowned" but is instead accessible to multiple users concurrently.
- 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.
- 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.
- Owned by another object ( composability like NFTs with metadata):
- One object can own another object. This can be used for nested compositions, building rich hierarchies.
- 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.
- 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 parametersreturn_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 transactionpublic
→ visible from outside the moduleT
→ generic type parameterctx
→ 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.