Move Execution
Sui Move, the object model, entry functions, dynamic fields, parallel execution, and gas metering.
Third-party documentation. This is independently authored analysis of the public Sui codebase — not the official docs, and not reviewed or endorsed by the Sui team.
Move Execution
Transactions on Sui are executed by the MoveVM, running Sui Move — a flavor of the Move language adapted to Sui's object model. This module covers how objects, ownership, and execution work from a builder's perspective.
Objects
An object is a typed value with:
id— a globally uniqueUID, assigned by the framework.owner— an address, another object, or the special "shared" state.version— bumped on every mutation (this powers fast-path conflict detection).- a data layout — defined by the struct that declares
keyability.
Structs with the key ability are objects; structs with store can be embedded in other objects (and made transferable). Assets like coins are not just balances — each coin is an object you can see, query, split, and merge.
Ownership rules
- Owned by an address — only that address's transaction (or a transaction it authorized) can mutate the object; it's a mutable reference passed by
&mut. - Owned by another object — the parent object's owner controls it.
- Shared — anyone can read, and mutation goes through consensus. Sharing is an explicit choice (
transfer::share_object), and it's the main "this is a protocol, not a wallet" switch you flip in your design.
Entry functions
The public interface of a Move package is its entry functions — functions marked entry that a transaction can call by name. They must be public, take &mut TxContext if they create objects, and cannot return values (their "output" is state changes and events). This is the Sui equivalent of Solidity's external functions.
Dynamic fields
Fixed structs are great for hot paths but inflexible. Dynamic fields let an object store arbitrary key/value pairs (dynamic_field::add/remove/borrow), which is how Sui builds things like open collections, user profiles, and lazily-materialized storage. They have a small cost and require care (they're not type-checked by the framework), but they remove the "I need a registry contract" problem.
Execution and parallelism
- Transactions are executed by validators in batches, with independent objects executed in parallel; the MoveVM's determinism guarantees every validator reaches the same state.
- Gas is metered per command and per byte (storage costs matter — see the Storage module).
- Effects are deterministic: same tx block + same input objects = same state, always.
Differences from upstream Move (Diem/Aptos)
- No global storage — you can't read "any address's balance by address"; you must hold the object or query via the object store.
- Ownership by value — ownership is a property of the object itself, enforced by the runtime, not by module conventions.
- Entry functions + TxContext — the calling convention differs; packages written for Aptos won't compile unchanged.
- No
move_to/move_fromon arbitrary addresses — resources live as objects, managed by modules that hold them.
Edge cases
- Creating an object and then not transferring it in the same transaction leaves it owned by the transaction sender — actually the default, and often the wrong default; be explicit about destination.
- Objects with
storecan be wrapped into other objects; a wrapped object is not independently queryable (its content is nested) — read via the parent. - Dynamic field keys collide by (type, name) — two modules adding the same key string to the same object conflict; namespace your keys.
Interface
// Sui Move: objects own data; modules define objects + entry functions.module examples::counter {use sui::object::{Self, UID};use sui::transfer;use sui::tx_context::{Self, TxContext};/// A simple owned object: nobody but its owner can touch it/// without an explicit transfer of the object reference.public struct Counter has key, store {id: UID,value: u64,}/// `entry` makes this callable from a transaction, not just Move./// It must take `&mut TxContext` and (for creating objects) ctx.public entry fun create(ctx: &mut TxContext) {transfer::transfer(Counter { id: object::new(ctx), value: 0 },tx_context::sender(ctx),);}public entry fun increment(c: &mut Counter) {c.value = c.value + 1;}}
