The Aptos Blockchain

by Greg Nazario


Welcome to the official guide for developers building on the Aptos blockchain.

The Aptos blockchain is a high-performance, scalable, and secure platform designed to support a wide range of decentralized applications (dApps) and services. It is built on the Move programming language, a safe and flexible language originally developed for the Diem project.

Key Features

  • Move Language: A resource-oriented programming language with strong safety guarantees enforced at compile time.
  • High Throughput: Parallel execution via BlockSTM enables high transaction throughput.
  • Low Latency: Sub-second transaction finality.
  • Upgradeable Contracts: Compatible upgrade policies allow bug fixes and feature additions.
  • On-Chain Randomness: Built-in randomness for games, lotteries, and fair selection.
  • AI-Ready: Documentation published in llms.txt format, with a growing ecosystem of AI agent frameworks.

Who This Book Is For

This book is for developers who want to build on Aptos. It covers everything from setting up your development environment and learning the fundamentals of Move to mastering advanced topics and design patterns. Whether you are writing code by hand or using AI-assisted development tools, this book provides the comprehensive reference you need.

Ready to get started?

Introduction

The Aptos Book extends the Move book, offering a single resource for design patterns, usage examples, and other information needed to build on Aptos. It provides context for building on Aptos and can be used entirely offline.

Get Started

You can jump to any section for a specific topic, but the material is arranged so that readers first learn Aptos fundamentals and then progress to writing Move contracts.

We recommend working through the chapters in order. Alternatively, use the search bar to find specific topics or keywords.

Book Structure

The book is organized into several parts:

  1. Getting Started - Installation, first contract, CLI basics
  2. Core Language - Variables, types, functions, ownership, structs, enums, modules
  3. Standard Libraries - Collections, error handling, generics, testing
  4. Blockchain Concepts - Events, parallelization, data models, storage, gas
  5. Design Patterns - Best practices and common patterns
  6. Building with LLMs - Using AI tools for Aptos development
  7. Reference - Standard libraries, glossary, appendix

LLM-Optimized

This book is optimized for consumption by Large Language Models (LLMs). It is published in the llms.txt format alongside the standard HTML, making it directly usable as context for AI-assisted development. See the Building on Aptos with LLMs chapter for details.

Contribution

Contributions are welcome. Please open a GitHub issue or pull request and:

  1. Identify the section being updated
  2. Provide a concise description of the change
  3. Note any gaps or missing areas

Getting Started

Let's get you started with the Aptos Blockchain! We'll discuss:

  1. Installing the Aptos CLI on macOS, Linux, and Windows.
  2. Hello Blockchain! - a simple contract that writes and reads data on-chain.
  3. Hello Aptos CLI! - a simple example of using the Aptos CLI to interact with the blockchain.

Installation

The first step is to install the Aptos CLI. The CLI is a command-line interface(CLI) tool that allows you to interact with the Aptos blockchain, including compiling, testing, and deploying Move modules, managing accounts, and reading and writing data to the blockchain.

Note: If you are using an unsupported platform or configuration, or you prefer to build a specific version from source, you can follow the Building from Source guide on the Aptos developer docs.

The following instructions will tell you how to install the latest version of the Aptos CLI. It is highly recommended to always use the latest version of the CLI, as it contains the latest features and bug fixes.

Installing the Aptos CLI with Homebrew on macOS

For macOS, it's recommended to use Homebrew. To install the Aptos CLI on macOS, if you have Homebrew installed, you can use the following command:

brew install aptos

Installing the Aptos CLI on macOS and Linux

To install the Aptos CLI on macOS and Linux, you can use the following command:

curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh

Installing the Aptos CLI on Windows with Winget

For Windows users, you can use the Windows Package Manager (Winget) to install the Aptos CLI. Open a command prompt or PowerShell and run the following command:

winget install aptos.aptos-cli

Installing the Aptos CLI on Windows

To install the Aptos CLI on Windows, you can use the following command in PowerShell:

iwr "https://aptos.dev/scripts/install_cli.ps1" -useb | iex

Troubleshooting

To check whether you have the Aptos CLI installed correctly, open a shell and enter this line:

aptos --version

You should see output similar to the following, with your CLI version.

aptos 7.6.0

If you see this information, you have installed the Aptos CLI successfully! If you don’t see this information, check that the Aptos CLI is in your %PATH% system variable as follows.

In Windows CMD, use:

echo %PATH%

In PowerShell, use:

echo $env:Path

In Linux and macOS, use:

echo $PATH

Updating the Aptos CLI

To update the Aptos CLI to the latest version, you can use the same command you used to install it. For example, if you installed the CLI using Homebrew, you can run:

brew upgrade aptos

If you installed the CLI using the curl command, you can run the aptos update command:

aptos update aptos

Alternatively, you can run the installation command again:

curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh

Local Documentation

The Aptos CLI also provides local documentation that you can access by running the following command:

aptos --help

This command will display a list of available commands and options for the Aptos CLI, along with a brief description of each command. You can also access the documentation for a specific command by running:

aptos <command> --help

Text Editors and Integrated Development Environments

This book makes no assumptions about what tools you use to author Move code. Just about any text editor will get the job done! However, many text editors and integrated development environments (IDEs) have built-in support for Move:

  • VS Code: Search for "Aptos Move Analyzer" in the VS Code Extensions Marketplace for syntax highlighting, go-to-definition, auto-completion, and error checking.
  • IntelliJ IDEA: Search for "Move Language" in the JetBrains Plugin Marketplace for IDE support.
  • Vim/Neovim: Community-maintained Move syntax highlighting plugins are available.
  • Emacs: Community-maintained move-mode packages are available.

For the best development experience, VS Code with the Aptos Move Analyzer extension is recommended.

Hello Blockchain!

Let's start with the simplest example, which shows you how to:

  1. Build and publish a contract
  2. Write data on-chain
  3. Read data on-chain

Deploying your first contract

First, we'll create an account on devnet to deploy your contract, we'll name it the profile tutorial:

aptos init --profile tutorial --network devnet --assume-yes

Next, open a new test folder, then call the command aptos move init --template to initialize a test.

mkdir hello_blockchain
cd hello_blockchain
aptos move init --template hello-blockchain

This will create a folder structure like so:

hello_blockchain
├── Move.toml
├── sources
├── scripts
└── tests

Then, the run next command to simply build and publish the contract:

aptos move publish --profile tutorial --named-addresses hello_blockchain=tutorial

Note that named-addresses sets the named address hello_blockchain in the Move.toml file to the tutorial profile created in the CLI.

Great! You've deployed your first contract! You can view it on the Aptos Explorer by searching for your account address and selecting the appropriate network.

Running the app

Now that you've deployed your first contract, let's interact with it.

Let's create your first message on-chain by calling an entry function:

aptos move run --profile tutorial --function-id tutorial::message::set_message --args 'string:Hello world!'

Note that this runs the entry function hello_blockchain::set_message on the contract you just deployed. It then provides the first non-signer argument as a string.

Once this is run successfully, you can view the on-chain state, with a view function:

aptos move view --profile tutorial --function-id tutorial::message::get_message --args address:tutorial

This will return the value Hello world! you just wrote on-chain. Congrats you've just written and read your first data on-chain! You can continue to run those two functions to write and read from the on-chain state respectively.

For more information about this, let's dive into the full contract and a breakdown next.

Full Contract

Below is the full contract for hello_blockchain.

/// Writes a message to a single storage slot, all changes overwrite the previous.
/// Changes are recorded in `MessageChange` events.
module hello_blockchain::message {
    use std::error;
    use std::signer;
    use std::string::{Self, String};
    use aptos_framework::event;
    #[test_only]
    use std::debug::print;

    /// A resource for a single storage slot, holding a message.
    struct MessageHolder has key {
        message: String,
    }

    #[event]
    /// Event representing a change in a message, records the old and new messages, and who wrote it.
    struct MessageChange has drop, store {
        account: address,
        from_message: String,
        to_message: String,
    }

    /// The address does not contain a MessageHolder
    const E_NO_MESSAGE: u64 = 1;

    #[view]
    /// Reads the message from storage slot
    public fun get_message(addr: address): String acquires MessageHolder {
        assert!(exists<MessageHolder>(addr), error::not_found(E_NO_MESSAGE));
        MessageHolder[addr].message
    }

    /// Sets the message to the storage slot
    public entry fun set_message(account: signer, message: String) acquires MessageHolder {
        let account_addr = signer::address_of(&account);
        if (!exists<MessageHolder>(account_addr)) {
            move_to(&account, MessageHolder {
                message,
            })
        } else {
            let message_holder = &mut MessageHolder[account_addr];
            let from_message = message_holder.message;
            event::emit(MessageChange {
                account: account_addr,
                from_message,
                to_message: message,
            });
            message_holder.message = message;
        }
    }

    #[test(account = @0x1)]
    fun sender_can_set_message(account: signer) acquires MessageHolder {
        let msg: String = string::utf8(b"Running test sender_can_set_message...");
        print(&msg);

        let addr = signer::address_of(&account);
        aptos_framework::account::create_account_for_test(addr);
        set_message(account, string::utf8(b"Hello, Blockchain"));

        assert!(get_message(addr) == string::utf8(b"Hello, Blockchain"));
    }
}

Breakdown

Module

The first 3 lines here, define documentation and the name of the module. Here you can see that the /// represents a doc comment. Documentation can be generated from these comments, where /// describes what's directly below it.

module hello_blockchain::message represents the name of the address, and the name of the module. hello_blockchain is what we call a named address. This named address can be passed in at compile time, and determines where the contract is being deployed. message is the name of the module. By convention, these are lowercased.

/// Writes a message to a single storage slot, all changes overwrite the previous.
/// Changes are recorded in `MessageChange` events.
module hello_blockchain::message {}

Imports

Next, we import some libraries that we will use in our contract. The use keyword is used to import modules, and are of the form use <module_address>::<module_name>. There are three standard library addresses that we can use from Aptos:

  • std - The standard library, which contains basic functionality like strings, vectors, and events.
  • aptos_std - The Aptos standard library, which contains functionality specific to the Aptos blockchain, like string manipulation.
  • aptos_framework - The Aptos framework, which contains functionality specific to the Aptos framework, like events, objects, accounts and more.

Note that the #[test_only] attribute is used to indicate that the module is only for testing purposes, and will not be compiled into the non-test bytecode. This is useful for debugging and testing purposes.

use std::error;
use std::signer;
use std::string::{Self, String};
use aptos_framework::event;
#[test_only]
use std::debug::print;

Structs

Next, we define a struct that will hold our message. Structs are structured data that is a collection of other types. These types can be primitives (e.g. u8, bool, address) or other structs. In this case, the struct is called MessageHolder, and it has a single field to hold the message.

/// A resource for a single storage slot, holding a message.
struct MessageHolder has key {
    message: String,
}

Events

Next, we define an event that will be emitted when the message is changed. Events are used to record changes to the blockchain in an easily indexable way. They are similar to events in other programming languages, and can be used to log changes to the blockchain. In this case, the event is called MessageChange, and it has three fields: account, which is the address of the account that changed the message, from_message, which is the old message, and to_message, which is the new message. The has drop, store attributes are required for events and indicate that the event can be dropped from scope and stored in the blockchain.

Events are defined with the #[event] annotation, and is required to emit as an event.

#[event]
/// Event representing a change in a message, records the old and new messages, and who wrote it.
struct MessageChange has drop, store {
    account: address,
    from_message: String,
    to_message: String,
}

Note that the doc comments /// must be directly above the struct, and not before the annotations. This differs from Rust, which allows the annotation in either location.

Constants and Error messages

Next is specifically a constant. Constants in Move must have a type definition, and can only be primitive types. They can also be documented with a doc comment. In this case, it will be used as an error, which is can define a user defined message when aborting. By convention, these start with a E_ or E, and the doc comment will define the abort error message. We'll show how these are used later.

/// The address does not contain a MessageHolder
const E_NO_MESSAGE: u64 = 1;

View Functions and Reading State

View functions are how external callers can easily read state from the blockchain. As you can see here the function get_message allows for outputting the message stored on chain. The #[view] annotation marks a function as callable from outside of the Move VM. Without this, the function won't be able to be called by aptos move view or the SDKs.

You can see here we define a function as public, which means it can be called by other Move functions within the Move VM. You can see it takes a single address argument, which determines the location that the hello_blockchain message is stored. Additionally, you can see String which is the return value of the function. Lastly, in the function signature acquires MessageHolder shows that the function accesses global state of the MessageHolder.

#[view]
/// Reads the message from storage slot
public fun get_message(addr: address): String acquires MessageHolder {
    // ...
}

The function body is fairly simple, only 2 lines. First, there's an assert! statement, which defines an error condition. The error condition shows that if the MessageHolder is not at the address, it will throw the error specified earlier in the constant.

This is then followed by accessing the message directly from that on-chain state, and returning a copy to the user.

#[view]
/// Reads the message from storage slot
public fun get_message(addr: address): String acquires MessageHolder {
    assert!(exists<MessageHolder>(addr), error::not_found(E_NO_MESSAGE));
    MessageHolder[addr].message
}

Note that return values of view functions must have the abilities copy and drop

Entry Functions and Writing State

Entry functions are the way that users can call a function as a standalone transaction. The set_message function, is denoted in the function signature as entry which means it can be called as an entry function payload, a standalone transaction.

Similar to the view function, you will see here, that it has two input arguments. The first argujment is the signer, which can only be provided by the transaction signature or similar authorization mechanism. The signer authorizes the function to move global state to the address. The second argument is a String and is the message to be written to the blockchain.

/// Sets the message to the storage slot
public entry fun set_message(account: signer, message: String) acquires MessageHolder {

Note that an entry function does not always need to be public, it can be private, which is without the public in front. This can be useful to ensure that the function cannot be called from within another Move function.

Note also that signer can also be &signer, and as the first argument to the entry function. Requiring more than one signer as arguments, means that it needs to be a multi-agent transaction, or simply signed by more than one party.

Now for the function body, we have a few parts. We can see that we get the address of the account calling the transaction, the signer. Once we get this address, we can check if the MessageHolder resource (aka struct) is stored in global storage for this account. If it hasn't been initialized (there is no resource), we move_to the account, a new MessageHolder resource. This adds the resource to the account to read later, with the initial message.

/// Sets the message to the storage slot
public entry fun set_message(account: signer, message: String) acquires MessageHolder {
    let account_addr = signer::address_of(&account);
    if (!exists<MessageHolder>(account_addr)) {
        move_to(&account, MessageHolder {
            message,
        })

Finally, if the resource already exists, we handle it smoothly by just updating the state of the resource to use the new message. When we do this, we also emit an event for easy indexing of changes of the message.

/// Sets the message to the storage slot
public entry fun set_message(account: signer, message: String) acquires MessageHolder {
    let account_addr = signer::address_of(&account);
    if (!exists<MessageHolder>(account_addr)) {
        move_to(&account, MessageHolder {
            message,
        })
    } else {
        let message_holder = &mut MessageHolder[account_addr];
        let from_message = message_holder.message;
        event::emit(MessageChange {
            account: account_addr,
            from_message,
            to_message: message,
        });
        message_holder.message = message;
    }
}

Note that the signer is not required to change existing state on a resource. This is because only the module that owns the struct can modify the state of the resource. However, the signer is always required to authorize storing state into an address.

Unit Tests

Move has native built in unit testing. To add a test, simply add the #[test] annotation to a function. As you can see in the function signature, you can define predefined addresses to create signers for the test. In this case, the account name is shared, and it is creating a signer for the 0x1 address. The test can then simply call any of the other functions in the module, and assert behaviors afterwards.

    #[test(account = @0x1)]
    fun sender_can_set_message(account: signer) acquires MessageHolder {
        let msg: String = string::utf8(b"Running test sender_can_set_message...");
        print(&msg);

        let addr = signer::address_of(&account);
        aptos_framework::account::create_account_for_test(addr);
        set_message(account, string::utf8(b"Hello, Blockchain"));

        assert!(get_message(addr) == string::utf8(b"Hello, Blockchain"));
    }

Note that tests can be placed into separate test files in the tests folder; However private functions cannot be called outside of the same file. If you want to make test only functions that can be used in other modules, simply add the #[test_only] annotation.

Hello Aptos CLI!

Introduction

The Aptos CLI is a powerful command-line interface that allows you to interact with the Aptos blockchain. It provides a wide range of commands to manage accounts, deploy smart contracts, and perform various operations on the blockchain.

You will become familiar with it the longer you develop on the Aptos blockchain, as it is a key tool for compiling, testing, deploying, and interacting with contracts on the Aptos blockchain.

Installation

If you haven't already installed the Aptos CLI, please refer to the Installation guide for instructions on how to install it on your system. The CLI is available for macOS, Linux, and Windows for both x86 and ARM systems.

Hello Aptos CLI

In this section, we will explore some basic commands of the Aptos CLI to interact with the blockchain. We will cover submitting transactions and querying accounts.

Initializing the CLI

First, determine whether you want a workspace style config or a global style config. By default, it uses workspace which means credentials and configuration will be based in your current directory. When you change directories, you will not use those credentials, and then will need to create different ones for each directory. If you want a global config that is shared among all directories, run the following command:

aptos config set-global-config --config-type global

Once we've set that, we will need to create the default profile for the Aptos CLI. This profile will store your account keys and create an account for you to use with the CLI. To initialize the CLI, run the following command in your terminal:

aptos init

This command will prompt you to create a new account if you have not already created one. It will look something like this:

$ aptos init
Configuring for profile default
Choose network from [devnet, testnet, mainnet, local, custom | defaults to devnet]

You can choose the network you want to connect to. For this example, we will use the devnet network, which is a short lived test network that is reset once a week. It is a great place to start experimenting with the Aptos CLI and easy to ignore your early mistakes. You can just press Enter to select the default devnet network.

Next, it will prompt you to enter your private key. If you do not have a private key, you can generate a new one by just pressing Enter. If you have a private key, you can enter it as a hex literal (starting with 0x). The prompt will look like this:

Enter your private key as a hex literal (0x...) [Current: Redacted | No input: Generate new key (or keep one if present)]

The CLI will then automatically fund and create an account for you on the devnet network. You will see output similar to the following:

Account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a is not funded, funding it with 100000000 Octas
Account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a funded successfully

---
Aptos CLI is now set up for account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a as profile default!
---

{
  "Result": "Success"
}

This means that your account is now set up and ready to use with the Aptos CLI!

Similarly, you can create additional profiles by running the aptos init command with the --profile <profile_name>. Then any of those accounts can be used with the CLI by specifying the --profile <profile_name> flag in your commands.

Additionally, if you want to change the network for your profile, you can use the aptos init command again to change the network:

aptos init --profile default --network <network_name>

Submitting a Transaction

Now that we have initialized the CLI and created an account, we can submit a transaction to the blockchain. For this example, we will send some Octas (subunit of the native APT token of the Aptos blockchain) from our account to another account.

To send Octas, we will use the aptos account transfer command. This command allows you to send funds to an account on the blockchain. The command syntax is as follows:

aptos account transfer --account <recipient_address> --amount <amount>

Note that the --account flag is used to specify the recipient's address, and the --amount flag is used to specify the amount to send in Octas. For example, to send 10 Octas to the account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a, you would run:

aptos account transfer --account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a --amount 10

Programming a Guessing Game

We're going to dive deep into Move with a simple example, a single player guessing game.

The rules are simple:

  1. Pass in a number to guess
  2. The same account will guess until they get the same number
  3. When it's guessed, the game is over and can be reset.
  4. All the guesses will be kept track of, and each number can only be guessed once.

Note that this is pretty trivial from a guessing game, as the number is on-chain and public information, but it can be good for learning.

Guessing Game Contract

Creating a module

First, we need to create a module. A module, in Move is a collection of related code (structs, functions, constants) that is published at a specific address. Modules provide encapsulation and organization for your smart contract logic.

module module_addr::guessing_game {

}

Contract Explanation and Breakdown

Creating a Module

A module in Move is a collection of related code (structs, functions, constants) that is published at a specific address. Modules provide encapsulation and organization for your smart contract logic.

module module_addr::guessing_game {
    // Module contents go here
}

Here, module_addr is a named address (set at compile/deploy time), and guessing_game is the module's name.


Error Codes

The contract defines error constants to provide clear, descriptive error messages for different failure conditions:

const E_NO_GAME: u64 = 1;         // No game initialized for this account
const E_GAME_OVER: u64 = 2;       // Game is over, must reset
const E_GAME_NOT_OVER: u64 = 3;   // Game is not over, must finish guessing
const E_GAME_INITIALIZED: u64 = 4;// Game already initialized
const E_ALREADY_GUESSED: u64 = 5; // Number already guessed

Game State

The Game struct stores all state for a single game:

struct Game has key {
    number: u8,             // The number to guess
    guesses: vector<u8>,    // All previous guesses
    game_over: bool,        // Whether the game is over
}
  • The key ability allows this struct to be stored in global storage under an account address.

Game Functions

Creating a Game

entry fun create_game(caller: &signer, number: u8) {
    let caller_addr = signer::address_of(caller);
    assert!(!exists<Game>(caller_addr), E_GAME_INITIALIZED);
    move_to(caller, Game {
        number,
        guesses: vector[],
        game_over: false,
    })
}
  • Only one game per account is allowed at a time.
  • Stores the new game in the caller's account.

Making a Guess

public entry fun guess(caller: &signer, number: u8) acquires Game {
    let caller_addr = signer::address_of(caller);
    assert!(exists<Game>(caller_addr), E_NO_GAME);
    let game = &mut Game[caller_addr];
    assert!(!game.game_over, E_GAME_OVER);
    assert!(!game.guesses.contains(&number), E_ALREADY_GUESSED);
    game.guesses.push_back(number);
    if (number == game.number) {
        game.game_over = true;
    }
}
  • Checks that the game exists and is not over.
  • Prevents duplicate guesses.
  • Marks the game as over if the guess is correct.

Resetting the Game

entry fun reset_game(caller: &signer, new_num: u8) acquires Game {
    let caller_addr = signer::address_of(caller);
    assert!(exists<Game>(caller_addr), E_NO_GAME);
    let game = &mut Game[caller_addr];
    assert!(game.game_over, E_GAME_NOT_OVER);
    game.game_over = false;
    game.guesses = vector[];
    game.number = new_num;
}
  • Only allowed if the game is over.
  • Resets guesses and sets a new number.

Removing the Game State

entry fun remove_state(caller: &signer) acquires Game {
    let caller_addr = signer::address_of(caller);
    assert!(exists<Game>(caller_addr), E_NO_GAME);
    let Game { .. } = move_from<Game>(caller_addr);
}
  • Deletes the game from storage for the caller.

View Functions

These functions allow anyone to query the game state without modifying it:

#[view]
public fun is_game_over(addr: address): bool acquires Game {
    assert!(exists<Game>(addr), E_NO_GAME);
    Game[addr].game_over
}

#[view]
public fun number(addr: address): u8 acquires Game {
    assert!(exists<Game>(addr), E_NO_GAME);
    assert!(is_game_over(addr), E_GAME_NOT_OVER);
    Game[addr].number
}

#[view]
public fun guesses(addr: address): vector<u8> acquires Game {
    assert!(exists<Game>(addr), E_NO_GAME);
    Game[addr].guesses
}
  • is_game_over: Returns whether the game is over.
  • number: Returns the answer, but only if the game is over (prevents cheating).
  • guesses: Returns all guesses made so far.

Key Concepts Demonstrated

  • State Management: Uses a struct with the key ability for on-chain state.
  • Access Control: Only the account owner can modify their game.
  • Error Handling: Uses clear error codes and assertions.
  • Resource Safety: Ensures proper creation, update, and deletion of game state.
  • View Functions: Provides read-only access to game state.
  • Game Logic: Implements a simple, fair guessing game with validation.

This contract is a practical example of how to build a stateful, interactive Move smart contract on Aptos, and demonstrates best practices for error handling, state management, and user interaction.

Here is the full contract at the end:

/// Guessing game
///
/// - Call `create_game` to start the game
/// - Guess with `guess`
/// - When the number is guessed correctly, the game is over
/// - Can be reset with `reset_game`
/// - Can be cleaned up with `remove_state`
///
module module_addr::guessing_game {
    use std::signer;

    /// No game initialized for this account, please call create_game first
    const E_NO_GAME: u64 = 1;

    /// Game is over, please call reset_game
    const E_GAME_OVER: u64 = 2;

    /// Game is not over,  please guess until the game is over
    const E_GAME_NOT_OVER: u64 = 3;

    /// Game is already initialized, please guess until the game is over
    const E_GAME_INITIALIZED: u64 = 4;

    /// Number is already guessed, please guess a different number
    const E_ALREADY_GUESSED: u64 = 5;

    /// State of the game
    struct Game has key {
        number: u8,
        guesses: vector<u8>,
        game_over: bool,
    }

    /// Creates a game
    entry fun create_game(caller: &signer, number: u8) {
        let caller_addr = signer::address_of(caller);
        assert!(!exists<Game>(caller_addr), E_GAME_INITIALIZED);
        move_to(caller, Game {
            number,
            guesses: vector[],
            game_over: false,
        })
    }

    /// Guesses on the game
    public entry fun guess(caller: &signer, number: u8) acquires Game {
        let caller_addr = signer::address_of(caller);

        // Check that the game exists
        assert!(exists<Game>(caller_addr), E_NO_GAME);

        let game = &mut Game[caller_addr];

        // Check that the game isn't over
        assert!(!game.game_over, E_GAME_OVER);

        // Check that I haven't guessed already
        assert!(!game.guesses.contains(&number), E_ALREADY_GUESSED);

        game.guesses.push_back(number);

        // Check win condition
        if (number == game.number) {
            game.game_over = true;
        }
    }

    /// Resets the game when it's done
    entry fun reset_game(caller: &signer, new_num: u8) acquires Game {
        let caller_addr = signer::address_of(caller);

        // Check that the game exists
        assert!(exists<Game>(caller_addr), E_NO_GAME);

        let game = &mut Game[caller_addr];

        // Check that the game is over
        assert!(game.game_over, E_GAME_NOT_OVER);

        game.game_over = false;
        game.guesses = vector[];
        game.number = new_num;
    }

    /// Deletes the game state
    entry fun remove_state(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);
        assert!(exists<Game>(caller_addr), E_NO_GAME);
        let Game {
            ..
        } = move_from<Game>(caller_addr);
    }

    #[view]
    public fun is_game_over(addr: address): bool acquires Game {
        assert!(exists<Game>(addr), E_NO_GAME);

        Game[addr].game_over
    }

    #[view]
    public fun number(addr: address): u8 acquires Game {
        assert!(exists<Game>(addr), E_NO_GAME);
        assert!(is_game_over(addr), E_GAME_NOT_OVER);

        Game[addr].number
    }

    #[view]
    public fun guesses(addr: address): vector<u8> acquires Game {
        assert!(exists<Game>(addr), E_NO_GAME);

        Game[addr].guesses
    }
}

Testing the Contract

Move provides a unit testing framework that's really easy to test different functionality. The tests can simply be written in Move alongside the code in the same module.

All unit tests can be run from the Aptos CLI by using the command aptos move test. For this example, we will specifically use the --dev flag to fill in the address, with the full command being aptos move test --dev

Testing successful functionality

To define a test, you simply need to add #[test] above a function. The function will only be used for testing, so it is best to keep them as private functions (no public or entry).

module module_addr::guessing_game {

    // ... contract definition ...

    #[test]
    fun test_flow(caller: &signer) acquires Game {}
}

Within the #[test] annotation, you can add any number of signers to be created for the test. In this example, we'll only create one signer named caller. This signer will be used to call the functions in the contract and store state at the associated address 0x1337.

Note that you can use either named addresses e.g. @module_addr or an address literal @0x1337. @ denotes that the literal is an address.

module module_addr::guessing_game {

    // ... contract definition ...

    #[test(caller = @0x1337)]
    fun test_flow(caller: &signer) acquires Game {}
}

Within the test, you can call any move functions that exist, and use assertions similar to in the regular code.

Note that you can skip the error codes for assertions in tests. Here, we'll test the basic flow of creating a game and guessing correctly on the first try. You can see how the game is considered over, and the one guess is recorded.

module module_addr::guessing_game {

    // ... contract definition ...

    #[test(caller = @0x1337)]
    fun test_flow(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);
        create_game(caller, 1);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[]);

        guess(caller, 1);
        assert!(is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[1]);
    }
}

You can test this now by running the below command:

aptos move test --dev

And this testing can be added to in order to make it more extensive and include resetting the game:

module module_addr::guessing_game {

    // ... contract definition ...

    #[test(caller = @0x1337)]
    fun test_flow(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);
        create_game(caller, 1);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[]);

        guess(caller, 1);
        assert!(is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[1]);

        reset_game(caller, 2);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[]);

        guess(caller, 3);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[3]);

        guess(caller, 4);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[3, 4]);

        guess(caller, 2);
        assert!(is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[3, 4, 2]);
    }
}

Testing failures

Failures can similarly be tested, by checking for abort codes. These can be added by adding the #[expected_failure(abort_code = ...)].

Note that the abort code can either be a constant e.g. E_ALREADY_GUESSED or the number directly 5.

module module_addr::guessing_game {

    // ... contract definition ...

    #[test(caller = @0x1337)]
    #[expected_failure(abort_code = E_ALREADY_GUESSED)]
    fun test_double_guess(caller: &signer) acquires Game {}
}

If you run this test and there is no abort in the test, it will fail. Meanwhile, we can add the actual functionality into the test. When the below test is run, it will pass with a failure on the second guess call.

module module_addr::guessing_game {

    // ... contract definition ...

    #[test(caller = @0x1337)]
    #[expected_failure(abort_code = E_ALREADY_GUESSED)]
    fun test_double_guess(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);
        create_game(caller, 1);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[]);

        guess(caller, 2);
        guess(caller, 2);
    }
}

For more specific abort checking, you can add the location of the abort:

module module_addr::guessing_game {
    #[test(caller = @0x1337)]
    #[expected_failure(abort_code = E_ALREADY_GUESSED, location = module_addr::guessing_game)]
    fun test_double_guess(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);
        create_game(caller, 1);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[]);

        guess(caller, 2);
        guess(caller, 2);
    }
}

Full Example

The full tests written are here:

module module_addr::guessing_game {

    // ... contract definition ...

    #[test(caller = @0x1337)]
    /// Tests the full flow of the game
    fun test_flow(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);
        create_game(caller, 1);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[]);

        guess(caller, 1);
        assert!(is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[1]);

        reset_game(caller, 2);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[]);

        guess(caller, 3);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[3]);

        guess(caller, 4);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[3, 4]);

        guess(caller, 2);
        assert!(is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[3, 4, 2]);
    }

    #[test(caller = @0x1337)]
    #[expected_failure(abort_code = E_ALREADY_GUESSED, location = module_addr::guessing_game)]
    /// Tests guessing the same number
    fun test_double_guess(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);
        create_game(caller, 1);
        assert!(!is_game_over(caller_addr));
        assert!(guesses(caller_addr) == vector[]);

        guess(caller, 2);
        guess(caller, 2);
    }
}

Deploying the Contract

To deploy the contract, we'll use the Aptos CLI. We'll assume you have already run the command aptos init for the default profile. If you have not, go check out the hello_aptos_cli tutorial.

In order to deploy the contract, we'll run the below command that will compile the contract, and then ask you if you're sure that you want to deploy it.

aptos move deploy --named-addresses module_addr=default

Once it's deployed, you should be able to see your contract on your network and your address in the Aptos Explorer. This uploads your source code by default, so it's easily verifiable and easy to tell which functions are which.

Upgrading the Contract

To upgrade the contract, simply run the command again:

aptos move deploy --named-addresses module_addr=default

Interacting with the Contract

Now that the guessing game contract is deployed, we can interact with it using the Aptos CLI. We'll assume you're using the default profile created earlier with aptos init.

The guessing game module was published under your account's address, so all function calls can be made using:

--function-id default::guessing_game::<function_name>

The default profile automatically provides the signer argument, so we only need to supply the non-signer parameters.

Creating a Game

To start a new game, call the create_game entry function and provide a number (0–255) that players must guess:

aptos move run \
  --profile default \
  --function-id default::guessing_game::create_game \
  --args 'u8:5'

This initializes a new game for the signer with the answer set to 5.

Making a Guess

Once a game is created, you can make guesses by calling the guess function:

aptos move run \
  --profile default \
  --function-id default::guessing_game::guess \
  --args 'u8:3'

You can continue guessing with different values:

aptos move run \
  --profile default \
  --function-id default::guessing_game::guess \
  --args 'u8:5'

If the guess matches the stored answer, the game is marked as over.

Viewing Game State

The contract provides several view functions, which allow anyone to inspect the game state without modifying it.

Is the game over?

aptos move view \
  --profile default \
  --function-id default::guessing_game::is_game_over \
  --args address:default

View the answer (only allowed when the game is over)

aptos move view \
  --profile default \
  --function-id default::guessing_game::number \
  --args address:default

View all guesses made so far

aptos move view \
  --profile default \
  --function-id default::guessing_game::guesses \
  --args address:default

Here, address:default automatically resolves to the signer’s on-chain account address.

Resetting or Removing the Game

Once a game has been completed, it can be reset with a new number:

aptos move run \
  --profile default \
  --function-id default::guessing_game::reset_game \
  --args 'u8:7'

To remove the game state entirely from your account:

aptos move run \
  --profile default \
  --function-id default::guessing_game::remove_state

This clears the Game resource from storage and allows a completely new game to be created.

Extending the Contract

Adding Randomness

Providing the number to guess is kind of silly, let's add a function to generate a random number and insert it into the contract. The backwards compatibility rules say that we cannot remove existing functions, so we will just abort on the existing functions. This should keep it clean to remove warnings, and allow for upgrading.

module module_addr::guessing_game {

    // ... contract ...

    /// Manual inputs are no longer supported
    const E_NO_LONGER_SUPPORTED: u64 = 6;

    entry fun create_game(_caller: &signer, _num: u8) {
        abort E_NO_LONGER_SUPPORTED
    }

    entry fun reset_game(_caller: &signer, _num: u8) {
        abort E_NO_LONGER_SUPPORTED
    }
}

We will then add new functions create_game_random and reset_game_random which mirror the original functions but only use random inputs.

module module_addr::guessing_game {
    use aptos_framework::randomness;

    #[randomness]
    entry fun create_game_random(caller: &signer) {
        let caller_addr = signer::address_of(caller);
        assert!(!exists<Game>(caller_addr), E_GAME_INITIALIZED);
        let number = randomness::u8_integer();
        move_to(caller, Game {
            number,
            guesses: vector[],
            game_over: false,
        })
    }

    #[randomness]
    entry fun reset_game_random(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);

        // Check that the game exists
        assert!(exists<Game>(caller_addr), E_NO_GAME);

        let game = &mut Game[caller_addr];

        // Check that the game is over
        assert!(game.game_over, E_GAME_NOT_OVER);

        game.game_over = false;
        game.guesses = vector[];
        game.number = randomness::u8_integer();
    }
}

But, of course the tests now fail! We will need to update the existing tests given our expectations. This can be done by either adding a #[test_only] function, or by setting the seed for the randomness in tests.

We can add a #[test_only] helper function that lets us create a game with a known number for testing:

module module_addr::guessing_game {
    // ... existing code ...

    #[test_only]
    fun create_game_for_test(caller: &signer, number: u8) {
        let caller_addr = signer::address_of(caller);
        assert!(!exists<Game>(caller_addr), E_GAME_INITIALIZED);
        move_to(caller, Game {
            number,
            guesses: vector[],
            game_over: false,
        })
    }

    #[test_only]
    fun reset_game_for_test(caller: &signer, new_num: u8) acquires Game {
        let caller_addr = signer::address_of(caller);
        assert!(exists<Game>(caller_addr), E_NO_GAME);
        let game = &mut Game[caller_addr];
        assert!(game.game_over, E_GAME_NOT_OVER);
        game.game_over = false;
        game.guesses = vector[];
        game.number = new_num;
    }

    #[test(caller = @0x1337)]
    fun test_flow(caller: &signer) acquires Game {
        let caller_addr = signer::address_of(caller);
        create_game_for_test(caller, 1);
        assert!(!is_game_over(caller_addr));

        guess(caller, 1);
        assert!(is_game_over(caller_addr));

        reset_game_for_test(caller, 2);
        assert!(!is_game_over(caller_addr));

        guess(caller, 2);
        assert!(is_game_over(caller_addr));
    }
}

The #[test_only] attribute ensures these helper functions are excluded from the production bytecode. They give us a deterministic way to test the game logic without relying on randomness.

Once this is done, we can simply upgrade the contract by deploying again.

aptos move deploy --named-addresses module_addr=default

Conclusion

The guessing game example has given a full demo of creating a contract, testing a contract, deploying, and extending a contract. To learn more, and check out more information about Move contracts, checkout the rest of the book and these resources and more:

Common Programming Concepts

Introduction

This chapter covers the fundamental programming concepts that form the foundation of Move development on Aptos. These concepts are essential for writing safe, efficient, and maintainable smart contracts.

Definition 1.1 (Programming Concept) A programming concept is a fundamental idea or principle that guides how we structure and organize code to solve problems effectively.

Core Concepts Overview

The common programming concepts in Move include:

  1. Variables: Named storage locations that hold values
  2. Data Types: Classifications that define the nature and operations of values
  3. Functions: Reusable blocks of code that perform specific tasks
  4. Comments: Documentation that explains code behavior
  5. Control Flow: Mechanisms that determine the order of execution

Move-Specific Considerations

Theorem 1.1 (Move Safety Properties) Move enforces several safety properties that distinguish it from other programming languages:

  • Resource Safety: Resources cannot be duplicated or lost
  • Type Safety: All operations are type-checked at compile time
  • Memory Safety: No null pointers, dangling references, or buffer overflows
  • Thread Safety: All values are immutable by default

Resource-Oriented Programming

Definition 1.2 (Resource) A resource is a special type in Move that represents digital assets with strict ownership semantics. Resources:

  • Cannot be copied
  • Cannot be dropped
  • Must be explicitly moved or destroyed
// Example: A simple resource representing a digital coin
struct Coin has key {
    value: u64,
    owner: address,
}

// Resources must be explicitly handled
public fun create_coin(value: u64, owner: address): Coin {
    Coin { value, owner }
}

public fun destroy_coin(coin: Coin) {
    // Explicit destruction - no automatic cleanup
    let Coin { value: _, owner: _ } = coin;
}

Abilities System

Definition 1.3 (Ability) An ability is a property that can be attached to types to specify what operations are allowed:

  • key: Can be stored as a top-level value
  • store: Can be stored inside other values
  • copy: Can be copied
  • drop: Can be dropped

Theorem 1.2 (Ability Composition) The abilities system ensures that:

  • Resources (no copy, no drop) maintain their safety properties
  • Ordinary values can be freely manipulated
  • Storage operations are explicit and controlled

Learning Path

This chapter is organized to build understanding progressively:

  1. Variables: Start with basic value storage and manipulation
  2. Data Types: Understand the type system and available types
  3. Functions: Learn to organize code into reusable units
  4. Comments: Document code for maintainability
  5. Control Flow: Control program execution flow

Best Practices

Principle 1.1 (Move Best Practices) When writing Move code:

  1. Use Resources for Assets: Represent digital assets as resources
  2. Minimize Abilities: Only add abilities when necessary
  3. Explicit Ownership: Make ownership transfers explicit
  4. Type Safety: Leverage the type system for correctness
  5. Documentation: Comment complex logic and invariants

Common Pitfalls

Warning 1.1 (Common Mistakes) Avoid these common mistakes in Move:

  1. Forgetting Abilities: Not adding required abilities to types
  2. Resource Leaks: Creating resources without proper cleanup
  3. Type Mismatches: Using wrong types in operations
  4. Unsafe References: Creating references that could become invalid
  5. Missing Error Handling: Not handling potential failures

Tools and Development

Definition 1.4 (Development Tools) Essential tools for Move development:

  • Move Compiler: Type checking and compilation
  • Move Prover: Formal verification
  • Aptos CLI: Deployment and interaction
  • IDE Support: Syntax highlighting and error detection

Conclusion

Understanding these common programming concepts is essential for effective Move development. The Move language's unique features, particularly its resource system and abilities, provide powerful safety guarantees but require careful attention to detail.

The following chapters will explore each concept in depth, providing practical examples and best practices for writing secure and efficient smart contracts.

Exercises

  1. Exercise 1.1: Create a simple resource type with appropriate abilities
  2. Exercise 1.2: Identify the abilities required for different use cases
  3. Exercise 1.3: Analyze the safety properties of a given Move program
  4. Exercise 1.4: Compare Move's type system with other programming languages

References

  1. Move Language Documentation
  2. Aptos Developer Resources
  3. Move Prover User Guide
  4. Smart Contract Security Best Practices

Variables

Variables are fundamental building blocks in Move programs that allow you to store and manipulate data. Understanding how to create and manage variables is essential for writing effective Move code.

Variable Declaration

Basic Variable Declaration

Variables in Move are declared using the let keyword, followed by the variable name and an optional type annotation.

module my_module::variables {
    public fun basic_variables() {
        // Variable with type inference
        let x = 42;
        
        // Variable with explicit type annotation
        let y: u64 = 100;
        
        // Variable with explicit type and suffix
        let z = 255u8;
        
        // Boolean variable
        let is_active = true;
        
        // String variable
        let message = string::utf8(b"Hello, Move!");
        
        // Address variable
        let account_addr = @0x1;
    }
}

Variable Declaration with Different Types

public fun variable_types() {
    // Integer types
    let small_number: u8 = 255;
    let medium_number: u16 = 65535;
    let large_number: u64 = 18446744073709551615;
    let very_large_number: u128 = 340282366920938463463374607431768211455u128;
    
    // Boolean type
    let flag: bool = true;
    
    // Address type
    let addr: address = @0xABCDEF;
    
    // Vector type
    let numbers: vector<u64> = vector[1, 2, 3, 4, 5];
    
    // String type
    let text: String = string::utf8(b"Move programming");
    
    // Tuple type
    let pair: (u64, String) = (42, string::utf8(b"answer"));
}

Variable Assignment and Mutability

Immutable Variables (Default)

By default, variables in Move are immutable, meaning they cannot be changed after declaration.

public fun immutable_variables() {
    let x = 42;
    // x = 50; // This would cause a compilation error
    
    let message = string::utf8(b"Hello");
    // message = string::utf8(b"World"); // This would cause a compilation error
}

Mutable Variables

To create a mutable variable, use the mut keyword.

public fun mutable_variables() {
    // Mutable variable declaration
    let mut x = 42;
    x = 50; // Now this is allowed
    
    let mut counter = 0u64;
    counter = counter + 1;
    counter = counter + 1;
    
    // Mutable vector
    let mut numbers = vector[1, 2, 3];
    vector::push_back(&mut numbers, 4);
    vector::push_back(&mut numbers, 5);
    
    // Mutable string
    let mut message = string::utf8(b"Hello");
    message = message + string::utf8(b" World");
}

Variable Reassignment

public fun variable_reassignment() {
    let mut value = 10;
    
    // Reassign with different value
    value = 20;
    
    // Reassign with calculation
    value = value * 2;
    
    // Reassign with function result
    value = add(value, 5);
    
    // Reassign with conditional
    if (value > 50) {
        value = 100;
    } else {
        value = 0;
    };
}

fun add(a: u64, b: u64): u64 {
    a + b
}

Variable Scoping

Local Variables

Variables declared within a function are local to that function and are automatically dropped when the function ends.

public fun local_variables() {
    let x = 10; // Local variable
    
    {
        let y = 20; // Local variable in inner scope
        let z = x + y; // Can access outer scope variable
        // z is dropped at end of inner scope
    };
    
    // y is not accessible here
    // z is not accessible here
    
    // x is still accessible
    let result = x * 2;
}

Variable Shadowing

Move allows variable shadowing, where a new variable with the same name can be declared in an inner scope.

public fun variable_shadowing() {
    let x = 10;
    
    {
        let x = 20; // Shadows the outer x
        // x is 20 in this scope
    };
    
    // x is 10 again in outer scope
}

Variable Lifetime

public fun variable_lifetime() {
    let mut counter = 0u64;
    
    while (counter < 5) {
        let temp = counter * 2; // temp is created in each iteration
        counter = counter + 1;
        // temp is dropped at end of each iteration
    };
    
    // counter is still available here
    let final_value = counter;
}

Variable Initialization

Immediate Initialization

Variables must be initialized when declared.

public fun immediate_initialization() {
    // All variables must be initialized
    let x = 42;
    let y: u64 = 100;
    let flag = true;
    let message = string::utf8(b"Hello");
}

Conditional Initialization

Variables can be initialized conditionally.

public fun conditional_initialization() {
    let condition = true;
    
    let value = if (condition) {
        100
    } else {
        200
    };
    
    // value is now either 100 or 200
}

Initialization with Function Calls

public fun initialization_with_functions() {
    let result = calculate_value(10);
    let message = create_message(string::utf8(b"Hello"));
    let numbers = create_vector(5);
}

fun calculate_value(input: u64): u64 {
    input * 2
}

fun create_message(prefix: String): String {
    prefix + string::utf8(b" World")
}

fun create_vector(size: u64): vector<u64> {
    let mut vec = vector::empty<u64>();
    let i = 0;
    while (i < size) {
        vector::push_back(&mut vec, i);
        i = i + 1;
    };
    vec
}

Variable Patterns

Destructuring Assignment

Move supports destructuring assignment for tuples and structs.

public fun destructuring() {
    // Tuple destructuring
    let (x, y) = (10, 20);
    
    // Nested tuple destructuring
    let ((a, b), c) = ((1, 2), 3);
    
    // Struct destructuring (if you have a struct)
    // let Point { x, y } = Point { x: 10, y: 20 };
}

Multiple Variable Declaration

public fun multiple_variables() {
    // Declare multiple variables
    let x = 1;
    let y = 2;
    let z = 3;
    
    // Or use tuple destructuring
    let (a, b, c) = (1, 2, 3);
    
    // With different types
    let (number, text, flag) = (42, string::utf8(b"Hello"), true);
}

Variable Best Practices

Choose Descriptive Names

public fun good_naming() {
    // Good: Descriptive names
    let user_age = 25;
    let account_balance = 1000;
    let is_user_active = true;
    let user_name = string::utf8(b"Alice");
    
    // Bad: Unclear names
    let x = 25;
    let y = 1000;
    let flag = true;
    let s = string::utf8(b"Alice");
}

Use Appropriate Types

public fun appropriate_types() {
    // Use u8 for small numbers
    let age: u8 = 25;
    
    // Use u64 for most calculations
    let balance: u64 = 1000000;
    
    // Use u128 for large financial values
    let total_supply: u128 = 1000000000000000000u128;
    
    // Use bool for flags
    let is_enabled: bool = true;
    
    // Use address for account identifiers
    let user_address: address = @0x1;
}

Minimize Mutable Variables

public fun minimize_mutability() {
    // Good: Use immutable variables when possible
    let max_attempts = 3;
    let timeout_duration = 5000;
    
    // Only use mut when you need to change the value
    let mut current_attempt = 0;
    let mut elapsed_time = 0;
    
    while (current_attempt < max_attempts) {
        current_attempt = current_attempt + 1;
        elapsed_time = elapsed_time + 1000;
    };
}

Initialize Variables Close to Use

public fun initialize_close_to_use() {
    // Good: Initialize when needed
    let result = perform_calculation(10);
    
    if (result > 50) {
        let message = string::utf8(b"High result");
        // Use message here
    } else {
        let message = string::utf8(b"Low result");
        // Use message here
    };
    
    // Bad: Initialize too early
    // let message = string::utf8(b""); // Unused variable
    // let result = perform_calculation(10);
    // if (result > 50) {
    //     message = string::utf8(b"High result");
    // } else {
    //     message = string::utf8(b"Low result");
    // };
}

Common Variable Patterns

Counter Pattern

public fun counter_pattern() {
    let mut counter = 0u64;
    let max_count = 10;
    
    while (counter < max_count) {
        // Process something
        counter = counter + 1;
    };
}

Accumulator Pattern

public fun accumulator_pattern() {
    let numbers = vector[1, 2, 3, 4, 5];
    let mut sum = 0u64;
    let i = 0;
    let len = vector::length(&numbers);
    
    while (i < len) {
        let current = *vector::borrow(&numbers, i);
        sum = sum + current;
        i = i + 1;
    };
    
    // sum now contains the total
}

Flag Pattern

public fun flag_pattern() {
    let numbers = vector[1, 2, 3, 4, 5];
    let mut found = false;
    let mut found_value = 0u64;
    let target = 3;
    let i = 0;
    let len = vector::length(&numbers);
    
    while (i < len && !found) {
        let current = *vector::borrow(&numbers, i);
        if (current == target) {
            found = true;
            found_value = current;
        };
        i = i + 1;
    };
}

Temporary Variable Pattern

public fun temporary_variable_pattern() {
    let x = 10;
    let y = 20;
    
    // Use temporary variable for complex calculation
    let temp_sum = x + y;
    let result = temp_sum * 2;
    
    // Or for readability
    let base_value = calculate_base(x, y);
    let adjusted_value = apply_adjustment(base_value);
    let final_result = apply_discount(adjusted_value);
}

fun calculate_base(a: u64, b: u64): u64 {
    a + b
}

fun apply_adjustment(value: u64): u64 {
    value * 2
}

fun apply_discount(value: u64): u64 {
    value - (value / 10)
}

Variable Safety

Type Safety

Move's type system prevents many common programming errors.

public fun type_safety() {
    let x: u64 = 42;
    let y: u8 = 255;
    
    // Type conversion is explicit
    let z: u64 = x + (y as u64);
    
    // This would cause a compilation error:
    // let invalid = x + y; // Cannot add u64 and u8 directly
}

Bounds Checking

public fun bounds_checking() {
    let mut index = 0u64;
    let max_index = 10;
    
    // Safe bounds checking
    if (index < max_index) {
        index = index + 1;
    };
    
    // Safe array access (if you had an array)
    // if (index < vector::length(&array)) {
    //     let element = *vector::borrow(&array, index);
    // };
}

Null Safety

Move doesn't have null values, which prevents null pointer errors.

public fun null_safety() {
    // Move doesn't have null, so no null pointer errors
    let value: u64 = 42; // Always has a value
    
    // Use Option<T> for optional values
    // let optional_value: Option<u64> = option::some(42);
}

Performance Considerations

Variable Reuse

public fun variable_reuse() {
    let mut temp = 0u64;
    
    // Reuse variable for different purposes
    temp = calculate_value(10);
    // Use temp...
    
    temp = calculate_value(20);
    // Use temp again...
    
    temp = calculate_value(30);
    // Use temp one more time...
}

fun calculate_value(input: u64): u64 {
    input * 2
}

Avoid Unnecessary Variables

public fun avoid_unnecessary_variables() {
    // Good: Direct return
    let result = add(10, 20);
    
    // Bad: Unnecessary intermediate variable
    // let temp = add(10, 20);
    // let result = temp;
}

fun add(a: u64, b: u64): u64 {
    a + b
}

By understanding and following these variable management practices, you can write more readable, maintainable, and efficient Move code. Variables are the foundation of data manipulation in Move, and proper variable management is essential for building robust smart contracts.

Data Types

Move is a statically typed language, which means every variable and expression has a type that is known at compile time. Understanding the available data types is fundamental to writing effective Move code.

Primitive Types

Integer Types

Move provides several integer types with different sizes and characteristics.

Unsigned Integers

module my_module::integers {
    // 8-bit unsigned integer (0 to 255)
    public fun u8_example(): u8 {
        255u8
    }

    // 16-bit unsigned integer (0 to 65,535)
    public fun u16_example(): u16 {
        65535u16
    }

    // 32-bit unsigned integer (0 to 4,294,967,295)
    public fun u32_example(): u32 {
        4294967295u32
    }

    // 64-bit unsigned integer (0 to 18,446,744,073,709,551,615)
    public fun u64_example(): u64 {
        18446744073709551615u64
    }

    // 128-bit unsigned integer (0 to 2^128 - 1)
    public fun u128_example(): u128 {
        340282366920938463463374607431768211455u128
    }

    // 256-bit unsigned integer (0 to 2^256 - 1)
    public fun u256_example(): u256 {
        115792089237316195423570985008687907853269984665640564039457584007913129639935u256
    }
}

Integer Literals

public fun integer_literals() {
    // Default integer literals are u64
    let default_int: u64 = 42;
    
    // Explicit type annotations
    let small_int: u8 = 255u8;
    let medium_int: u16 = 65535u16;
    let large_int: u32 = 4294967295u32;
    let very_large_int: u128 = 340282366920938463463374607431768211455u128;
    
    // Hex literals
    let hex_u8: u8 = 0xFFu8;
    let hex_u64: u64 = 0xFFFFFFFFu64;
}

Integer Operations

public fun integer_operations() {
    let a: u64 = 10;
    let b: u64 = 3;
    
    // Arithmetic operations
    let sum = a + b;        // 13
    let difference = a - b;  // 7
    let product = a * b;     // 30
    let quotient = a / b;    // 3
    let remainder = a % b;   // 1
    
    // Bitwise operations
    let bitwise_and = a & b;  // 2
    let bitwise_or = a | b;   // 11
    let bitwise_xor = a ^ b;  // 9
    let left_shift = a << 2;  // 40
    let right_shift = a >> 1; // 5
    
    // Comparison operations
    let is_equal = a == b;     // false
    let is_not_equal = a != b; // true
    let is_greater = a > b;    // true
    let is_less = a < b;       // false
    let is_greater_equal = a >= b; // true
    let is_less_equal = a <= b;    // false
}

Boolean Type

The boolean type represents true or false values.

public fun boolean_examples() {
    // Boolean literals
    let true_value: bool = true;
    let false_value: bool = false;
    
    // Boolean operations
    let and_result = true && false;  // false
    let or_result = true || false;   // true
    let not_result = !true;          // false
    
    // Comparison results
    let comparison_result: bool = 5 > 3;  // true
    let equality_result: bool = 5 == 5;   // true
}

Address Type

Addresses represent 32-byte identifiers used for accounts, modules, and resources.

public fun address_examples() {
    // Address literals
    let account_address: address = @0x1;
    let custom_address: address = @0xABCDEF;
    let full_address: address = @0x0000000000000000000000000000000000000000000000000000000000000001;
    
    // Special addresses
    let zero_address: address = @0x0;
    let framework_address: address = @0x1;  // Aptos framework
    let token_address: address = @0x3;      // Legacy token standard
    let object_address: address = @0x4;     // Object standard
}

Vector Type

Vectors are dynamic arrays that can hold multiple values of the same type.

public fun vector_examples() {
    // Creating vectors
    let empty_vector: vector<u64> = vector::empty<u64>();
    let literal_vector: vector<u64> = vector[1, 2, 3, 4, 5];
    let string_vector: vector<String> = vector[
        string::utf8(b"hello"),
        string::utf8(b"world")
    ];
    
    // Vector operations
    let mut vec = vector[1, 2, 3];
    vector::push_back(&mut vec, 4);
    let length = vector::length(&vec);
    let first_element = *vector::borrow(&vec, 0);
    let last_element = vector::pop_back(&mut vec);
}

String Type

Strings are UTF-8 encoded text stored as vectors of bytes.

public fun string_examples() {
    // Creating strings
    let empty_string: String = string::utf8(b"");
    let hello_string: String = string::utf8(b"Hello, World!");
    let unicode_string: String = string::utf8(b"Hello 世界");
    
    // String operations
    let length = string::length(&hello_string);
    let is_empty = string::is_empty(&empty_string);
    let concatenated = string::utf8(b"Hello") + string::utf8(b" World");
    
    // String comparison
    let are_equal = hello_string == string::utf8(b"Hello, World!");
    let are_not_equal = hello_string != string::utf8(b"Goodbye");
}

Signer Type

The signer type represents the account that signed the transaction.

public fun signer_examples(account: &signer) {
    // Getting the address of the signer
    let account_address = signer::address_of(account);
    
    // Signer is used for resource operations
    // move_to(account, MyResource { data: 42 });
}

Type Abilities

Move types have abilities that determine how they can be used. The main abilities are:

  • copy: Can be copied
  • drop: Can be dropped (destroyed)
  • store: Can be stored in global storage
  • key: Can be used as a key in global storage

Ability Examples

module my_module::abilities {
    // Integer types have copy, drop, and store abilities
    public fun integer_abilities() {
        let x: u64 = 42;
        let y = x;  // Copy
        // x is still available due to copy ability
    }
    
    // Vector has copy, drop, and store abilities
    public fun vector_abilities() {
        let v1: vector<u64> = vector[1, 2, 3];
        let v2 = v1;  // Copy
        // v1 is still available
    }
    
    // String has copy, drop, and store abilities
    public fun string_abilities() {
        let s1: String = string::utf8(b"hello");
        let s2 = s1;  // Copy
        // s1 is still available
    }
    
    // Signer has drop ability only
    public fun signer_abilities(account: &signer) {
        let addr = signer::address_of(account);
        // Cannot copy signer, but can drop it
    }
}

Type Annotations

Move uses type annotations to specify the type of variables and function parameters.

public fun type_annotations() {
    // Explicit type annotations
    let explicit_u64: u64 = 42;
    let explicit_bool: bool = true;
    let explicit_address: address = @0x1;
    let explicit_vector: vector<u64> = vector[1, 2, 3];
    let explicit_string: String = string::utf8(b"hello");
    
    // Type inference (when possible)
    let inferred_u64 = 42;  // Inferred as u64
    let inferred_bool = true;  // Inferred as bool
    let inferred_vector = vector[1, 2, 3];  // Inferred as vector<u64>
}

Type Conversion

Move is strict about type conversions and requires explicit casting.

public fun type_conversion() {
    // Explicit casting between integer types
    let small: u8 = 255;
    let medium: u16 = (small as u16);
    let large: u64 = (medium as u64);
    
    // Casting to smaller types (may truncate)
    let large_num: u64 = 1000;
    let small_num: u8 = (large_num as u8);  // Will be 232 (1000 % 256)
    
    // Boolean to integer conversion
    let bool_val: bool = true;
    let int_val: u64 = (bool_val as u64);  // 1 for true, 0 for false
}

Generic Types

Move supports generic types for creating reusable code.

public fun generic_examples() {
    // Generic vector operations
    let int_vector: vector<u64> = vector[1, 2, 3];
    let string_vector: vector<String> = vector[
        string::utf8(b"hello"),
        string::utf8(b"world")
    ];
    
    // Generic function example
    let int_length = vector::length(&int_vector);
    let string_length = vector::length(&string_vector);
}

Type Safety

Move's type system prevents many common programming errors.

public fun type_safety_examples() {
    // This would cause a compilation error:
    // let x: u64 = "hello";  // Cannot assign string to u64
    
    // This would cause a compilation error:
    // let y: u8 = 256;  // Value too large for u8
    
    // This would cause a compilation error:
    // let z = x + "world";  // Cannot add u64 and string
    
    // Correct usage
    let x: u64 = 42;
    let y: u8 = 255;
    let z: u64 = x + (y as u64);
}

Best Practices

Choose Appropriate Types

public fun choose_types() {
    // Use u8 for small numbers (0-255)
    let age: u8 = 25;
    
    // Use u64 for most integer operations
    let balance: u64 = 1000000;
    
    // Use u128 for large financial calculations
    let total_supply: u128 = 1000000000000000000u128;
    
    // Use u256 for cryptographic operations
    let private_key: u256 = 0x1234567890ABCDEFu256;
}

Use Type Annotations for Clarity

public fun clear_types() {
    // Explicit types make code more readable
    let user_id: u64 = 12345;
    let is_active: bool = true;
    let user_address: address = @0xABCDEF;
    let user_name: String = string::utf8(b"Alice");
    
    // Avoid ambiguous literals
    let small_number: u8 = 42u8;  // Clear that this is u8
    let large_number: u128 = 1000000000000000000u128;  // Clear that this is u128
}

Handle Type Conversions Carefully

public fun safe_conversions() {
    let large: u64 = 1000;
    
    // Check bounds before converting to smaller types
    if (large <= 255) {
        let small: u8 = (large as u8);
        // Safe conversion
    } else {
        // Handle overflow case
        // abort 0
    }
    
    // Use appropriate types for calculations
    let a: u64 = 1000000;
    let b: u64 = 2000000;
    let result: u64 = a * b;  // May overflow u64
    
    // Consider using u128 for large calculations
    let safe_result: u128 = (a as u128) * (b as u128);
}

Use Vectors Efficiently

public fun vector_best_practices() {
    // Pre-allocate when you know the size
    let mut numbers = vector::empty<u64>();
    vector::push_back(&mut numbers, 1);
    vector::push_back(&mut numbers, 2);
    vector::push_back(&mut numbers, 3);
    
    // Use appropriate element types
    let small_numbers: vector<u8> = vector[1, 2, 3, 4, 5];
    let large_numbers: vector<u64> = vector[1000000, 2000000, 3000000];
    
    // Consider storage costs for large vectors
    // Each vector element in global storage uses a storage slot
}

Common Type Patterns

Error Handling with Types

enum Result<T> {
    Ok(T),
    Err(String),
}

public fun safe_divide(a: u64, b: u64): Result<u64> {
    if (b == 0) {
        return Result::Err(string::utf8(b"Division by zero"))
    };
    Result::Ok(a / b)
}

Option Type Pattern

enum Option<T> {
    Some(T),
    None,
}

public fun find_element(numbers: vector<u64>, target: u64): Option<u64> {
    let i = 0;
    let len = vector::length(&numbers);
    
    while (i < len) {
        let current = *vector::borrow(&numbers, i);
        if (current == target) {
            return Option::Some(current)
        };
        i = i + 1;
    };
    
    Option::None
}

Understanding these data types and their characteristics is essential for writing efficient and correct Move code. The type system helps prevent errors and makes code more maintainable and readable.

Functions

Functions are the building blocks of Move programs. They allow you to encapsulate logic, reuse code, and create modular, maintainable programs. Move provides several types of functions with different visibility levels and purposes.

Function Types and Visibility

Public Functions

Public functions can be called from outside the module.

module my_module::functions {
    /// Public functions can be called from outside the module.
    public fun add(x: u64, y: u64): u64 {
        x + y
    }

    /// Public functions can also be entry functions.
    public entry fun public_entry(account: &signer, value: u64) {
        let addr = signer::address_of(account);
        // Process the transaction
    }
}

Private Functions

Private functions can only be called from within the same module. Functions are private by default.

module my_module::private_functions {
    /// Private functions can only be called from within the module.
    fun multiply(x: u64, y: u64): u64 {
        x * y
    }

    /// Public function that uses private function
    public fun calculate_area(width: u64, height: u64): u64 {
        multiply(width, height)  // Call private function
    }

    /// Private helper function
    fun validate_input(value: u64): bool {
        value > 0 && value < 1000
    }

    public fun process_data(input: u64): u64 {
        if (validate_input(input)) {  // Call private function
            input * 2
        } else {
            0
        }
    }
}

Friend Functions

Friend functions can be called from other modules that declare this module as a friend.

module my_module::friend_example {
    /// Friend functions can be called from other modules that declare this module as a friend.
    friend fun sum_vector(vec: vector<u64>): u64 {
        let sum = 0u64;
        let i = 0;
        let len = vector::length(&vec);
        
        while (i < len) {
            sum = sum + *vector::borrow(&vec, i);
            i = i + 1;
        };
        
        sum
    }

    /// Alternative syntax for friend functions
    public(friend) fun do_something_friend(x: u64): u64 {
        x + 1
    }
}

/// Module that declares my_module::friend_example as a friend
module my_module::friend_caller {
    friend my_module::friend_example;

    public fun call_friend_function(): u64 {
        let numbers = vector[1, 2, 3, 4, 5];
        my_module::friend_example::sum_vector(numbers)
    }
}

Package Functions

Package functions can be called from within the package but not from outside.

module my_module::package_functions {
    /// Package functions can be called from within the package, but not from outside.
    package fun square(x: u64): u64 {
        x * x
    }

    /// Alternative syntax for package functions
    public(package) fun cube(x: u64): u64 {
        x * x * x
    }

    /// Public function that uses package function
    public fun calculate_power(base: u64, power: u8): u64 {
        if (power == 2) {
            square(base)  // Call package function
        } else if (power == 3) {
            cube(base)    // Call package function
        } else {
            base
        }
    }
}

Entry Functions

Entry functions are special functions that can be called as standalone transactions from outside the module.

module my_module::entry_functions {
    use std::signer;

    /// Entry functions can be called as standalone transactions.
    entry fun create_account(account: &signer) {
        let addr = signer::address_of(account);
        // Initialize account data
    }

    /// Entry functions can also be public, friend, or package.
    public entry fun transfer_funds(
        from: &signer,
        to: address,
        amount: u64
    ) {
        let from_addr = signer::address_of(from);
        // Transfer logic here
    }

    /// Entry functions with multiple parameters
    entry fun update_profile(
        account: &signer,
        name: String,
        age: u8
    ) {
        let addr = signer::address_of(account);
        // Update profile logic
    }

    /// Entry functions can return values (though they're typically void)
    entry fun calculate_and_return(x: u64, y: u64): u64 {
        x + y
    }
}

Entry Function Best Practices

module my_module::entry_best_practices {
    use std::signer;
    use std::error;

    const EINSUFFICIENT_FUNDS: u64 = 1;
    const EINVALID_AMOUNT: u64 = 2;

    /// Good: Entry function with proper validation
    entry fun safe_transfer(
        from: &signer,
        to: address,
        amount: u64
    ) {
        // Validate input
        assert!(amount > 0, error::invalid_argument(EINVALID_AMOUNT));
        assert!(to != @0x0, error::invalid_argument(3));
        
        let from_addr = signer::address_of(from);
        
        // Check balance (simplified)
        // let balance = get_balance(from_addr);
        // assert!(balance >= amount, error::invalid_state(EINSUFFICIENT_FUNDS));
        
        // Perform transfer
        // transfer_funds(from_addr, to, amount);
    }

    /// Good: Entry function with clear parameter names
    entry fun create_user_profile(
        account: &signer,
        user_name: String,
        user_age: u8,
        user_email: String
    ) {
        let user_address = signer::address_of(account);
        
        // Validate input
        assert!(user_age >= 13, error::invalid_argument(4));
        assert!(string::length(&user_name) > 0, error::invalid_argument(5));
        
        // Create profile
        // create_profile(user_address, user_name, user_age, user_email);
    }
}

View Functions

View functions are read-only functions that can be called without a transaction. They are marked with the #[view] attribute.

module my_module::view_functions {
    use std::signer;

    struct UserProfile has key, store {
        name: String,
        age: u8,
        email: String,
    }

    /// View function - can be called without a transaction
    #[view]
    public fun get_user_name(user_addr: address): String {
        assert!(exists<UserProfile>(user_addr), 0);
        borrow_global<UserProfile>(user_addr).name
    }

    /// View function that returns multiple values
    #[view]
    public fun get_user_profile(user_addr: address): (String, u8, String) {
        assert!(exists<UserProfile>(user_addr), 0);
        let profile = borrow_global<UserProfile>(user_addr);
        (profile.name, profile.age, profile.email)
    }

    /// View function with complex logic
    #[view]
    public fun is_user_adult(user_addr: address): bool {
        if (!exists<UserProfile>(user_addr)) {
            return false
        };
        
        let profile = borrow_global<UserProfile>(user_addr);
        profile.age >= 18
    }

    /// View function that doesn't access global storage
    #[view]
    public fun calculate_discount(price: u64, discount_percent: u8): u64 {
        let discount = (price * (discount_percent as u64)) / 100;
        price - discount
    }
}

View Function Best Practices

module my_module::view_best_practices {
    /// Good: View function with proper error handling
    #[view]
    public fun get_user_balance(user_addr: address): u64 {
        if (!exists<UserBalance>(user_addr)) {
            return 0
        };
        
        borrow_global<UserBalance>(user_addr).amount
    }

    /// Good: View function that validates input
    #[view]
    public fun calculate_compound_interest(
        principal: u64,
        rate: u64,
        time: u64
    ): u64 {
        // Validate input parameters
        assert!(principal > 0, 0);
        assert!(rate <= 100, 0);  // Rate as percentage
        assert!(time > 0, 0);
        
        // Calculate compound interest
        let rate_decimal = rate / 100;
        let amount = principal * ((1 + rate_decimal) ^ time);
        amount - principal
    }
}

Test-Only Functions

Test-only functions are only available during testing and are marked with the #[test_only] attribute.

module my_module::test_functions {
    use std::signer;

    /// Test-only functions are only available during testing.
    #[test_only]
    fun create_test_user(account: &signer, name: String): address {
        let addr = signer::address_of(account);
        // Create test user profile
        // move_to(account, UserProfile { name, age: 25, email: string::utf8(b"test@example.com") });
        addr
    }

    /// Test-only functions can be public
    #[test_only]
    public fun setup_test_environment(): vector<u64> {
        vector[1, 2, 3, 4, 5]
    }

    /// Test function that tests other functions
    #[test_only]
    public fun test_add_function() {
        let result = add(2, 3);
        assert!(result == 5, 0);
    }

    /// Test function with test account
    #[test(account = @0x1)]
    public fun test_with_account(account: signer) {
        let addr = signer::address_of(&account);
        // Test logic here
    }

    /// Private function used by tests
    fun add(x: u64, y: u64): u64 {
        x + y
    }
}

Test Function Best Practices

module my_module::test_best_practices {
    use std::signer;
    use std::error;

    const EINVALID_INPUT: u64 = 1;

    /// Good: Comprehensive test function
    #[test_only]
    public fun test_validation_function() {
        // Test valid input
        let result1 = validate_input(50);
        assert!(result1 == true, 0);
        
        // Test invalid input
        let result2 = validate_input(0);
        assert!(result2 == false, 0);
        
        let result3 = validate_input(1001);
        assert!(result3 == false, 0);
    }

    /// Good: Test function with multiple test cases
    #[test_only]
    public fun test_calculation_function() {
        // Test case 1: Normal calculation
        let result1 = calculate_discount(100, 10);
        assert!(result1 == 90, 0);
        
        // Test case 2: No discount
        let result2 = calculate_discount(100, 0);
        assert!(result2 == 100, 0);
        
        // Test case 3: Full discount
        let result3 = calculate_discount(100, 100);
        assert!(result3 == 0, 0);
    }

    /// Good: Test function with test account
    #[test(account = @0x1)]
    public fun test_account_operations(account: signer) {
        let addr = signer::address_of(&account);
        
        // Test account creation
        // create_account(&account);
        // assert!(exists<UserProfile>(addr), 0);
        
        // Test account operations
        // update_profile(&account, string::utf8(b"Test User"), 25);
        // let profile = get_user_profile(addr);
        // assert!(profile.0 == string::utf8(b"Test User"), 0);
    }

    /// Helper functions for tests
    fun validate_input(value: u64): bool {
        value > 0 && value <= 1000
    }

    fun calculate_discount(price: u64, discount_percent: u8): u64 {
        let discount = (price * (discount_percent as u64)) / 100;
        price - discount
    }
}

Inline Functions

Inline functions are small functions that can be inlined by the compiler for better performance.

module my_module::inline_functions {
    /// Inline function - compiler may inline this for better performance
    #[inline]
    public fun max(a: u64, b: u64): u64 {
        if (a > b) a else b
    }

    /// Inline function with multiple parameters
    #[inline]
    public fun clamp(value: u64, min: u64, max: u64): u64 {
        if (value < min) {
            min
        } else if (value > max) {
            max
        } else {
            value
        }
    }

    /// Inline function used in calculations
    #[inline]
    public fun is_even(n: u64): bool {
        n % 2 == 0
    }

    /// Public function that uses inline functions
    public fun process_numbers(a: u64, b: u64, c: u64): u64 {
        let max_value = max(a, b);  // Inline function call
        let clamped = clamp(c, 0, 100);  // Inline function call
        
        if (is_even(max_value)) {  // Inline function call
            max_value + clamped
        } else {
            max_value - clamped
        }
    }
}

Inline Function Best Practices

module my_module::inline_best_practices {
    /// Good: Small, simple inline functions
    #[inline]
    public fun square(x: u64): u64 {
        x * x
    }

    /// Good: Inline function for frequently used operations
    #[inline]
    public fun is_positive(n: u64): bool {
        n > 0
    }

    /// Good: Inline function for type conversions
    #[inline]
    public fun u8_to_u64(value: u8): u64 {
        (value as u64)
    }

    /// Avoid: Large inline functions (compiler may ignore inline hint)
    #[inline]
    public fun complex_calculation(a: u64, b: u64, c: u64): u64 {
        let temp1 = a * b;
        let temp2 = b * c;
        let temp3 = c * a;
        let result = temp1 + temp2 + temp3;
        result / 3
    }
}

Function Overloading and Generics

Move doesn't support function overloading, but you can use generics to create flexible functions.

module my_module::generic_functions {
    /// Generic function that works with any type that has copy, drop abilities
    public fun identity<T>(x: T): T {
        x
    }

    /// Generic function with constraints
    public fun add_generic<T: copy + drop>(a: T, b: T): T {
        // Note: This is a simplified example
        // In practice, you'd need to implement specific logic for each type
        a
    }

    /// Generic function for vector operations
    public fun get_first<T>(vec: &vector<T>): T {
        assert!(vector::length(vec) > 0, 0);
        *vector::borrow(vec, 0)
    }

    /// Generic function with multiple type parameters
    public fun create_pair<T, U>(first: T, second: U): (T, U) {
        (first, second)
    }
}

Function Abilities and Constraints

Functions can have ability constraints that determine what types they can work with.

module my_module::function_abilities {
    /// Function that requires copy ability
    public fun duplicate<T: copy>(x: T): (T, T) {
        (x, x)
    }

    /// Function that requires drop ability
    public fun consume<T: drop>(x: T) {
        // x is automatically dropped at the end
    }

    /// Function that requires store ability
    public fun store_value<T: store>(x: T) {
        // x can be stored in global storage
    }

    /// Function with multiple ability constraints
    public fun process_value<T: copy + drop + store>(x: T): T {
        // x can be copied, dropped, and stored
        x
    }
}

Best Practices Summary

Function Visibility

  1. Start with private: Make functions private by default
  2. Use public sparingly: Only expose what's necessary
  3. Use friend for related modules: When modules need to share functionality
  4. Use package for internal APIs: For functions used within the package

Entry Functions

  1. Validate input: Always validate parameters in entry functions
  2. Use clear names: Make function names descriptive
  3. Handle errors gracefully: Use proper error handling
  4. Keep them simple: Delegate complex logic to private functions

View Functions

  1. Mark with #[view]: Always mark read-only functions with #[view]
  2. Don't modify state: View functions should be pure
  3. Handle missing data: Return sensible defaults for missing data
  4. Optimize for queries: Make view functions efficient

Test Functions

  1. Use #[test_only]: Mark test functions appropriately
  2. Test edge cases: Include boundary condition tests
  3. Use descriptive names: Make test names clear
  4. Test error conditions: Include tests for error cases

Inline Functions

  1. Keep them small: Inline functions should be simple
  2. Use for hot paths: Inline frequently called functions
  3. Don't overuse: Let the compiler decide when to inline
  4. Profile first: Measure performance before optimizing

By understanding and using these different function types effectively, you can create well-structured, maintainable, and efficient Move programs.

Comments

Comments are essential for documenting your Move code and making it more readable and maintainable. Move supports several types of comments that serve different purposes.

Types of Comments

1. Documentation Comments (///)

Documentation comments use three forward slashes (///) and are used to generate documentation for your code. These comments describe what's directly below them.

/// Writes a message to a single storage slot, all changes overwrite the previous.
/// Changes are recorded in `MessageChange` events.
module hello_blockchain::message {
    /// A resource for a single storage slot, holding a message.
    struct MessageHolder has key {
        message: String,
    }

    /// Event representing a change in a message, records the old and new messages, and who wrote it.
    #[event]
    struct MessageChange has drop, store {
        account: address,
        from_message: String,
        to_message: String,
    }

    /// The address does not contain a MessageHolder
    const ENO_MESSAGE: u64 = 0;

    /// Reads the message from storage slot
    #[view]
    public fun get_message(addr: address): String acquires MessageHolder {
        assert!(exists<MessageHolder>(addr), error::not_found(ENO_MESSAGE));
        borrow_global<MessageHolder>(addr).message
    }

    /// Sets the message to the storage slot
    public entry fun set_message(account: signer, message: String)
    acquires MessageHolder {
        let account_addr = signer::address_of(&account);
        if (!exists<MessageHolder>(account_addr)) {
            move_to(&account, MessageHolder {
                message,
            })
        } else {
            let old_message_holder = borrow_global_mut<MessageHolder>(account_addr);
            let from_message = old_message_holder.message;
            event::emit(MessageChange {
                account: account_addr,
                from_message,
                to_message: copy message,
            });
            old_message_holder.message = message;
        }
    }
}

2. Single-Line Comments (//)

Single-line comments use two forward slashes (//) and are used for brief explanations or notes within your code.

module my_module::example {
    // This is a single-line comment
    public fun add(x: u64, y: u64): u64 {
        x + y // Add the two numbers together
    }

    public fun process_data(data: vector<u64>): u64 {
        let sum = 0u64;
        let i = 0;
        let len = vector::length(&data);
        
        while (i < len) {
            sum = sum + *vector::borrow(&data, i); // Add current element to sum
            i = i + 1; // Increment counter
        };
        
        sum
    }

    // TODO: Implement error handling for edge cases
    // FIXME: This function needs optimization for large datasets
    public fun complex_calculation(input: u64): u64 {
        // Validate input
        assert!(input > 0, 0);
        
        // Perform calculation
        let result = input * 2;
        
        result
    }
}

3. Multi-Line Comments (/* */)

Multi-line comments use /* to start and */ to end. They can span multiple lines and are useful for longer explanations.

module my_module::complex_example {
    /*
     * This is a multi-line comment that can span
     * multiple lines. It's useful for longer explanations
     * or when you need to comment out large blocks of code.
     */
    public fun complex_function(): u64 {
        /*
         * This function performs a complex calculation:
         * 1. Initialize variables
         * 2. Perform iterative computation
         * 3. Apply final transformation
         * 4. Return result
         */
        
        let result = 0u64;
        
        /* 
         * Commented out code block:
         * let temp = 100u64;
         * result = result + temp;
         */
        
        result
    }
}

Best Practices for Comments

Documentation Comments (///)

  1. Use for public APIs: Always document public functions, structs, and modules
  2. Be descriptive: Explain what the code does, not how it does it
  3. Include examples: When helpful, include usage examples
  4. Document parameters and return values: Explain what each parameter does and what the function returns
  5. Use consistent formatting: Keep documentation style consistent across your codebase
/// Calculates the sum of two unsigned 64-bit integers.
/// 
/// # Arguments
/// * `a` - The first number to add
/// * `b` - The second number to add
/// 
/// # Returns
/// The sum of `a` and `b`
/// 
/// # Example
/// ```
/// let result = add(5, 3); // Returns 8
/// ```
public fun add(a: u64, b: u64): u64 {
    a + b
}

Single-Line Comments (//)

  1. Explain complex logic: Use comments to explain non-obvious code
  2. Avoid obvious comments: Don't comment on what the code obviously does
  3. Use TODO/FIXME: Mark areas that need attention
  4. Keep comments up to date: Ensure comments reflect the current code
// Good: Explains complex logic
let hash = sha3_256::hash(&data); // Hash the data for verification

// Bad: Obvious comment
let sum = a + b; // Add a and b

// Good: Marks future work
// TODO: Add input validation for edge cases
public fun process_input(input: u64): u64 {
    input * 2
}

Multi-Line Comments (/* */)

  1. Use for block comments: When you need to comment out large sections
  2. Document complex algorithms: Explain multi-step processes
  3. Temporary code removal: Comment out code you might need later
/*
 * This algorithm implements the following steps:
 * 1. Validate input parameters
 * 2. Initialize data structures
 * 3. Perform iterative computation
 * 4. Apply post-processing
 * 5. Return final result
 */
public fun complex_algorithm(input: vector<u64>): u64 {
    // Implementation here
    0
}

Comment Guidelines

What to Comment

  1. Public APIs: Always document public functions, structs, and modules
  2. Complex logic: Explain non-obvious algorithms or business logic
  3. Assumptions: Document any assumptions your code makes
  4. Limitations: Note any limitations or edge cases
  5. Dependencies: Explain dependencies on external modules or systems

What Not to Comment

  1. Obvious code: Don't comment on what the code obviously does
  2. Outdated information: Don't leave comments that are no longer accurate
  3. Implementation details: Focus on what, not how (unless the how is complex)

Comment Style

  1. Be concise: Keep comments brief but informative
  2. Use proper grammar: Write comments in clear, grammatically correct language
  3. Be consistent: Use consistent terminology and style throughout your codebase
  4. Update with code: When you change code, update related comments

Examples in Context

Module Documentation

/// A simple banking module that allows users to deposit and withdraw funds.
/// 
/// This module provides basic banking functionality including:
/// * Account creation and management
/// * Deposit and withdrawal operations
/// * Balance checking
/// * Transaction history tracking
/// 
/// # Security
/// All operations require proper authorization and validation.
module my_bank::banking {
    use std::signer;
    use std::error;
    
    /// Error codes for banking operations
    const EINSUFFICIENT_FUNDS: u64 = 1;
    const EACCOUNT_NOT_FOUND: u64 = 2;
    
    /// Represents a user's bank account
    struct Account has key, store {
        balance: u64,
        owner: address,
    }
    
    /// Creates a new bank account for the given signer
    public entry fun create_account(account: &signer) {
        let account_addr = signer::address_of(account);
        move_to(account, Account {
            balance: 0,
            owner: account_addr,
        });
    }
    
    /// Deposits the specified amount into the account
    public entry fun deposit(account: &signer, amount: u64) acquires Account {
        let account_addr = signer::address_of(account);
        let account_ref = borrow_global_mut<Account>(account_addr);
        account_ref.balance = account_ref.balance + amount;
    }
}

Function Documentation

/// Calculates the factorial of a given number.
/// 
/// The factorial of a number n is the product of all positive integers
/// less than or equal to n. For example, 5! = 5 × 4 × 3 × 2 × 1 = 120.
/// 
/// # Arguments
/// * `n` - The number to calculate factorial for (must be <= 20)
/// 
/// # Returns
/// The factorial of n
/// 
/// # Panics
/// Panics if n > 20 due to u64 overflow
/// 
/// # Example
/// ```
/// let result = factorial(5); // Returns 120
/// ```
public fun factorial(n: u64): u64 {
    if (n <= 1) {
        return 1
    };
    
    let result = 1u64;
    let i = 2u64;
    
    while (i <= n) {
        result = result * i;
        i = i + 1;
    };
    
    result
}

Documentation Generation

Move documentation comments can be used to generate documentation for your modules. The /// comments are processed by documentation generators to create readable documentation for your codebase.

When writing documentation comments, remember that they should be:

  • Clear and concise: Easy to understand
  • Complete: Cover all important aspects
  • Accurate: Reflect the actual behavior of the code
  • Helpful: Provide value to developers using your code

By following these guidelines, you'll create well-documented, maintainable Move code that's easy for others to understand and use.

Control Flow

Control flow statements allow you to control the execution path of your Move programs. Move provides several control flow constructs that enable conditional execution, loops, and pattern matching.

If Statements

If statements allow you to execute different code blocks based on whether a condition is true or false.

Basic If Statement

module my_module::control_flow {
    public fun basic_if(x: u64): u64 {
        if (x > 10) {
            x * 2
        } else {
            x
        }
    }
}

If-Else Statement

public fun if_else_example(age: u64): String {
    if (age < 18) {
        string::utf8(b"Minor")
    } else if (age < 65) {
        string::utf8(b"Adult")
    } else {
        string::utf8(b"Senior")
    }
}

If Statement with Multiple Conditions

public fun complex_condition(x: u64, y: u64): bool {
    if (x > 0 && y > 0 && x + y < 100) {
        true
    } else {
        false
    }
}

If Statement with Function Calls

public fun validate_input(input: u64): bool {
    if (input == 0) {
        return false
    };
    
    if (input > 1000) {
        return false
    };
    
    true
}

While Loops

While loops execute a block of code repeatedly as long as a condition remains true.

Basic While Loop

public fun count_down(n: u64): u64 {
    let counter = n;
    while (counter > 0) {
        counter = counter - 1;
    };
    counter
}

While Loop with Break

public fun find_first_positive(numbers: vector<u64>): u64 {
    let i = 0;
    let len = vector::length(&numbers);
    let result = 0u64;
    
    while (i < len) {
        let current = *vector::borrow(&numbers, i);
        if (current > 0) {
            result = current;
            break
        };
        i = i + 1;
    };
    
    result
}

While Loop with Continue

public fun sum_positive_numbers(numbers: vector<u64>): u64 {
    let i = 0;
    let len = vector::length(&numbers);
    let sum = 0u64;
    
    while (i < len) {
        let current = *vector::borrow(&numbers, i);
        i = i + 1;
        
        if (current == 0) {
            continue
        };
        
        sum = sum + current;
    };
    
    sum
}

Infinite While Loop with Break

public fun find_element(numbers: vector<u64>, target: u64): bool {
    let i = 0;
    let len = vector::length(&numbers);
    
    while (true) {
        if (i >= len) {
            break false
        };
        
        let current = *vector::borrow(&numbers, i);
        if (current == target) {
            break true
        };
        
        i = i + 1;
    }
}

For Loops

For loops allow you to iterate over ranges or collections. Move supports for loops with range syntax.

Basic For Loop with Range

public fun sum_range(start: u64, end: u64): u64 {
    let sum = 0u64;
    for (i in start..end) {
        sum = sum + i;
    };
    sum
}

For Loop with Step

public fun sum_even_numbers(max: u64): u64 {
    let sum = 0u64;
    for (i in 0..max) {
        if (i % 2 == 0) {
            sum = sum + i;
        };
    };
    sum
}

For Loop with Vector Iteration

public fun sum_vector_elements(numbers: vector<u64>): u64 {
    let sum = 0u64;
    let len = vector::length(&numbers);
    
    for (i in 0..len) {
        let element = *vector::borrow(&numbers, i);
        sum = sum + element;
    };
    
    sum
}

For Loop with Early Exit

public fun find_index(numbers: vector<u64>, target: u64): u64 {
    let len = vector::length(&numbers);
    
    for (i in 0..len) {
        let current = *vector::borrow(&numbers, i);
        if (current == target) {
            return i
        };
    };
    
    len // Return length if not found
}

Nested For Loops

public fun matrix_sum(matrix: vector<vector<u64>>): u64 {
    let sum = 0u64;
    let rows = vector::length(&matrix);
    
    for (i in 0..rows) {
        let row = vector::borrow(&matrix, i);
        let cols = vector::length(row);
        
        for (j in 0..cols) {
            let element = *vector::borrow(row, j);
            sum = sum + element;
        };
    };
    
    sum
}

Match Statements

Match statements provide pattern matching capabilities, primarily used with enums. They allow you to execute different code based on the variant of an enum.

Basic Match Statement

module my_module::enums {
    enum Status {
        Active,
        Inactive,
        Pending,
    }
    
    public fun get_status_message(status: Status): String {
        match (status) {
            Status::Active => string::utf8(b"User is active"),
            Status::Inactive => string::utf8(b"User is inactive"),
            Status::Pending => string::utf8(b"User status is pending"),
        }
    }
}

Match Statement with Data-Carrying Enums

enum Result<T> {
    Ok(T),
    Err(String),
}

public fun handle_result(result: Result<u64>): String {
    match (result) {
        Result::Ok(value) => string::utf8(b"Success: ") + std::to_string(value),
        Result::Err(message) => string::utf8(b"Error: ") + message,
    }
}

Match Statement with Complex Patterns

enum Shape {
    Circle(u64),    // radius
    Rectangle(u64, u64), // width, height
    Square(u64),    // side
}

public fun calculate_area(shape: Shape): u64 {
    match (shape) {
        Shape::Circle(radius) => {
            // Approximate area calculation (π * r²)
            radius * radius * 3
        },
        Shape::Rectangle(width, height) => {
            width * height
        },
        Shape::Square(side) => {
            side * side
        },
    }
}

Match Statement with Guards

enum Number {
    Zero,
    Positive(u64),
    Negative(u64),
}

public fun classify_number(num: Number): String {
    match (num) {
        Number::Zero => string::utf8(b"Zero"),
        Number::Positive(value) => {
            if (value < 10) {
                string::utf8(b"Small positive")
            } else {
                string::utf8(b"Large positive")
            }
        },
        Number::Negative(value) => {
            if (value > 10) {
                string::utf8(b"Large negative")
            } else {
                string::utf8(b"Small negative")
            }
        },
    }
}

Match Statement with Default Case

enum Direction {
    North,
    South,
    East,
    West,
}

public fun get_direction_name(direction: Direction): String {
    match (direction) {
        Direction::North => string::utf8(b"North"),
        Direction::South => string::utf8(b"South"),
        Direction::East => string::utf8(b"East"),
        Direction::West => string::utf8(b"West"),
    }
}

Match Statement in Error Handling

enum ValidationResult {
    Valid,
    InvalidAge,
    InvalidName,
    InvalidEmail,
}

public fun validate_user(age: u64, name: String, email: String): ValidationResult {
    if (age < 18 || age > 120) {
        return ValidationResult::InvalidAge
    };
    
    if (string::length(&name) == 0) {
        return ValidationResult::InvalidName
    };
    
    if (string::length(&email) == 0) {
        return ValidationResult::InvalidEmail
    };
    
    ValidationResult::Valid
}

public fun get_validation_message(result: ValidationResult): String {
    match (result) {
        ValidationResult::Valid => string::utf8(b"User data is valid"),
        ValidationResult::InvalidAge => string::utf8(b"Age must be between 18 and 120"),
        ValidationResult::InvalidName => string::utf8(b"Name cannot be empty"),
        ValidationResult::InvalidEmail => string::utf8(b"Email cannot be empty"),
    }
}

Best Practices

If Statements

  1. Use early returns: Return early to reduce nesting
  2. Keep conditions simple: Break complex conditions into multiple if statements
  3. Use meaningful variable names: Make conditions self-documenting
// Good: Early return
public fun validate_input(input: u64): bool {
    if (input == 0) {
        return false
    };
    
    if (input > 1000) {
        return false
    };
    
    true
}

// Bad: Deep nesting
public fun validate_input_bad(input: u64): bool {
    if (input != 0) {
        if (input <= 1000) {
            return true
        } else {
            return false
        }
    } else {
        return false
    }
}

While Loops

  1. Ensure termination: Make sure loops will eventually terminate
  2. Use break for early exit: Use break instead of complex conditions
  3. Initialize variables properly: Initialize loop variables before the loop
// Good: Clear termination condition
public fun safe_loop(max_iterations: u64): u64 {
    let i = 0;
    while (i < max_iterations) {
        i = i + 1;
    };
    i
}

// Bad: Potential infinite loop
public fun unsafe_loop(): u64 {
    let i = 0;
    while (i >= 0) { // This will never be false for u64
        i = i + 1;
    };
    i
}

For Loops

  1. Use for loops for known ranges: Prefer for loops when you know the iteration count
  2. Avoid modifying loop variables: Don't modify the loop variable inside the loop
  3. Use meaningful variable names: Use descriptive names for loop variables
// Good: Clear iteration
public fun process_array(data: vector<u64>): u64 {
    let sum = 0u64;
    let len = vector::length(&data);
    
    for (index in 0..len) {
        let element = *vector::borrow(&data, index);
        sum = sum + element;
    };
    
    sum
}

Match Statements

  1. Handle all cases: Ensure all enum variants are covered
  2. Use meaningful patterns: Use descriptive variable names in patterns
  3. Keep match arms simple: Extract complex logic into separate functions
// Good: All cases handled
public fun process_status(status: Status): String {
    match (status) {
        Status::Active => string::utf8(b"Active"),
        Status::Inactive => string::utf8(b"Inactive"),
        Status::Pending => string::utf8(b"Pending"),
    }
}

// Bad: Missing case (this would cause a compilation error)
public fun process_status_bad(status: Status): String {
    match (status) {
        Status::Active => string::utf8(b"Active"),
        Status::Inactive => string::utf8(b"Inactive"),
        // Missing Status::Pending case
    }
}

Common Patterns

Error Handling Pattern

enum OperationResult {
    Success(u64),
    Failure(String),
}

public fun safe_divide(a: u64, b: u64): OperationResult {
    if (b == 0) {
        return OperationResult::Failure(string::utf8(b"Division by zero"))
    };
    
    OperationResult::Success(a / b)
}

public fun handle_division_result(result: OperationResult): String {
    match (result) {
        OperationResult::Success(value) => {
            string::utf8(b"Result: ") + std::to_string(value)
        },
        OperationResult::Failure(error) => {
            string::utf8(b"Error: ") + error
        },
    }
}

State Machine Pattern

enum GameState {
    Waiting,
    Playing,
    Paused,
    GameOver,
}

public fun update_game_state(current_state: GameState, action: String): GameState {
    match (current_state) {
        GameState::Waiting => {
            if (action == string::utf8(b"start")) {
                GameState::Playing
            } else {
                GameState::Waiting
            }
        },
        GameState::Playing => {
            if (action == string::utf8(b"pause")) {
                GameState::Paused
            } else if (action == string::utf8(b"end")) {
                GameState::GameOver
            } else {
                GameState::Playing
            }
        },
        GameState::Paused => {
            if (action == string::utf8(b"resume")) {
                GameState::Playing
            } else if (action == string::utf8(b"end")) {
                GameState::GameOver
            } else {
                GameState::Paused
            }
        },
        GameState::GameOver => GameState::GameOver,
    }
}

By understanding and using these control flow constructs effectively, you can write more expressive and maintainable Move code that handles complex logic and decision-making scenarios.

Understanding Ownership

Introduction

Ownership is one of the most distinctive and powerful features of the Move programming language. Unlike most programming languages that use garbage collection or manual memory management, Move enforces ownership rules at compile time, ensuring memory safety and preventing common programming errors.

Definition 2.1 (Ownership) Ownership is a set of rules that govern how values are managed in memory, ensuring that each value has exactly one owner at any given time.

Why Ownership Matters

Theorem 2.1 (Ownership Benefits) Move's ownership system provides several critical benefits:

  1. Memory Safety: No dangling references, use-after-free, or double-free errors
  2. Thread Safety: Values cannot be accessed from multiple threads simultaneously
  3. Resource Management: Resources are explicitly managed and cannot be lost
  4. Compile-Time Guarantees: All ownership violations are caught at compile time

Comparison with Other Languages

Definition 2.2 (Memory Management Approaches) Different programming languages use various approaches to memory management:

  • Garbage Collection (Java, Python): Automatic memory cleanup
  • Manual Management (C, C++): Developer responsible for allocation/deallocation
  • Ownership System (Move, Rust): Compile-time enforcement of ownership rules

Theorem 2.2 (Move vs. Other Languages) Move's ownership system is more restrictive than garbage-collected languages but provides stronger safety guarantees:

  • No Runtime Overhead: No garbage collection pauses
  • Deterministic Behavior: Predictable resource management
  • Explicit Control: Developer has full control over resource lifecycle

Core Ownership Rules

Principle 2.1 (Move Ownership Rules) Move enforces three fundamental ownership rules:

  1. Single Owner: Each value has exactly one owner
  2. Move Semantics: When a value is assigned or passed to a function, ownership is transferred
  3. Explicit Destruction: Values must be explicitly destroyed when no longer needed

Rule 1: Single Owner

// Each value has exactly one owner
let x = 5; // x owns the value 5
let y = x; // Ownership of 5 moves from x to y
// x is no longer valid here - cannot be used

Theorem 2.3 (Single Owner Invariant) At any point in program execution, each value has exactly one owner, preventing:

  • Multiple mutable references to the same data
  • Data races in concurrent code
  • Use-after-free errors

Rule 2: Move Semantics

Definition 2.3 (Move Operation) A move operation transfers ownership of a value from one variable to another, invalidating the original variable.

struct Resource has key {
    value: u64,
}

fun transfer_ownership(resource: Resource) -> Resource {
    // resource is moved into this function
    // The caller no longer owns it
    resource // Return transfers ownership back
}

fun example() {
    let r = Resource { value: 42 };
    let r2 = transfer_ownership(r); // r is moved, r2 now owns the resource
    // r is no longer valid here
}

Rule 3: Explicit Destruction

Definition 2.4 (Destruction) Values must be explicitly destroyed when they go out of scope or are no longer needed.

fun destroy_example() {
    let resource = Resource { value: 100 };
    // At the end of this function, resource must be destroyed
    // Move will enforce this at compile time
}

Ownership and References

Definition 2.5 (Reference) A reference is a borrowed view of a value that does not transfer ownership.

fun reference_example() {
    let x = 5;
    let y = &x; // y is a reference to x, x still owns the value
    // x is still valid here
}

Theorem 2.4 (Reference Rules) References in Move follow strict rules:

  1. Immutable References: Multiple immutable references can exist simultaneously
  2. Mutable References: Only one mutable reference can exist at a time
  3. No Dangling References: References cannot outlive the value they reference

Immutable References

fun immutable_references() {
    let x = 10;
    let ref1 = &x; // Immutable reference
    let ref2 = &x; // Another immutable reference
    // Both ref1 and ref2 can be used simultaneously
}

Mutable References

fun mutable_references() {
    let mut x = 10;
    let ref1 = &mut x; // Mutable reference
    // let ref2 = &mut x; // Error: cannot borrow x as mutable more than once
    // let ref3 = &x; // Error: cannot borrow x as immutable while borrowed as mutable
}

Ownership in Practice

Function Parameters and Return Values

Algorithm 2.1 (Ownership Transfer in Functions)

1. When a value is passed to a function:
   - Ownership is transferred to the function
   - The caller no longer owns the value
2. When a value is returned from a function:
   - Ownership is transferred to the caller
   - The function no longer owns the value
3. If a value is not returned:
   - It must be destroyed within the function
fun take_ownership(resource: Resource) {
    // resource is owned by this function
    // It will be destroyed when the function ends
}

fun give_ownership() -> Resource {
    let resource = Resource { value: 42 };
    resource // Ownership is transferred to the caller
}

fun borrow_reference(resource: &Resource) {
    // resource is borrowed, not owned
    // The caller still owns the original value
}

Structs and Ownership

Definition 2.6 (Struct Ownership) When a struct is moved, all of its fields are moved with it.

struct Person has key {
    name: vector<u8>,
    age: u64,
}

fun struct_ownership() {
    let person = Person {
        name: b"Alice",
        age: 30,
    };
    
    let person2 = person; // person is moved to person2
    // person is no longer valid
}

Common Ownership Patterns

Clone When Needed

Definition 2.7 (Clone Operation) Creating a copy of a value when you need to use it in multiple places.

fun clone_example() {
    let x = 5;
    let y = x; // x is moved to y
    let z = y; // y is moved to z
    // Need to clone if we want multiple copies
}

Borrow Instead of Move

Principle 2.2 (Borrowing Strategy) When possible, borrow values instead of taking ownership to avoid unnecessary moves.

fun process_data(data: &vector<u8>) {
    // Process data without taking ownership
}

fun caller() {
    let data = b"Hello, World!";
    process_data(&data); // Borrow data
    // data is still valid here
}

Ownership and Abilities

Theorem 2.5 (Ownership and Abilities Relationship) The ownership system works in conjunction with the abilities system:

  • copy ability: Allows values to be copied instead of moved
  • drop ability: Allows values to be automatically dropped
  • key ability: Allows values to be stored as top-level resources
  • store ability: Allows values to be stored inside other values
struct CopyableValue has copy, drop {
    value: u64,
}

fun copyable_example() {
    let x = CopyableValue { value: 10 };
    let y = x; // x is copied to y, both are valid
    let z = x; // x is copied to z, all three are valid
}

Best Practices

Principle 2.3 (Ownership Best Practices)

  1. Prefer Borrowing: Use references when you don't need ownership
  2. Explicit Moves: Make ownership transfers explicit and clear
  3. Minimize Copies: Avoid unnecessary copying of large data structures
  4. Plan Resource Lifecycle: Think about when resources should be created and destroyed
  5. Use Abilities Appropriately: Only add abilities when necessary

Common Pitfalls

Warning 2.1 (Ownership Mistakes) Common mistakes to avoid:

  1. Trying to Use Moved Values: Using variables after they've been moved
  2. Multiple Mutable References: Creating multiple mutable references to the same data
  3. Dangling References: Creating references that outlive their referents
  4. Forgetting to Destroy: Not properly destroying resources
  5. Unnecessary Moves: Moving values when borrowing would suffice

Conclusion

Ownership is a fundamental concept in Move that provides powerful safety guarantees. While it may seem restrictive at first, understanding and working with the ownership system leads to safer, more predictable code.

The ownership system, combined with Move's type system and abilities, creates a robust foundation for building secure smart contracts. The next chapters will explore how ownership interacts with other Move features and provide practical examples of ownership patterns.

Exercises

  1. Exercise 2.1: Create a function that takes ownership of a resource and returns it
  2. Exercise 2.2: Implement a function that borrows a value instead of taking ownership
  3. Exercise 2.3: Identify ownership violations in a given Move program
  4. Exercise 2.4: Design a struct that demonstrates different ownership patterns

References

  1. Move Language Specification
  2. Move Ownership and References Guide
  3. Smart Contract Security Best Practices
  4. Move Prover and Formal Verification

Ownership Basics

Understanding the basics of ownership is crucial for writing Move code. This section covers the fundamental rules and concepts that govern how ownership works in Move.

What is Ownership?

Ownership is Move's most distinctive feature and the key to its memory safety guarantees. The ownership system consists of a set of rules that the compiler checks at compile time. No runtime overhead is incurred for any of the ownership features.

The Rules of Ownership

  1. Each value has a variable that's called its owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value will be dropped

Let's explore these rules through examples.

Variable Scope

A scope is the range within a program for which an item is valid. Let's look at an example:

module my_module::scope_example {
    public fun scope_demo() {
        // s is not valid here, it's not yet declared
        let s = string::utf8(b"hello");   // s is valid from this point forward

        // do stuff with s
        let len = string::length(&s);
    }   // this scope is now over, and s is no longer valid
}

In other words, there are two important points in time here:

  • When s comes into scope, it is valid
  • It remains valid until it goes out of scope

The String Type

To illustrate the rules of ownership, we need a more complex data type than the ones we covered in the previous sections. The types covered previously are all a known size, can be stored on the stack and popped off the stack when their scope is over, and can be quickly and trivially copied to make a new, independent instance if another part of code needs to use the same value in a different scope.

But we want to look at data that is stored on the heap and explore how Move knows when to clean up that data, and the String type is a perfect example. We'll concentrate on the parts of String that relate to ownership. These aspects also apply to other complex data types, whether they are provided by the standard library or created by you.

Memory and Allocation

In the case of a string literal, we know the contents at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient. But these properties only come from the string literal's immutability. Unfortunately, we can't put a blob of memory into the binary for each piece of text whose size is unknown at compile time and whose size might change while running the program.

With the String type, in order to support a mutable, growable piece of text, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the contents. This means:

  • The memory must be requested from the memory allocator at runtime
  • We need a way of returning this memory to the allocator when we're done with our String

The first part is done by us: when we call string::utf8(), its implementation requests the memory it needs. This is pretty much universal in programming languages.

However, the second part is different. In languages with a garbage collector (GC), the GC keeps track of and cleans up memory that isn't being used anymore, and we don't need to think about it. In most languages without a GC, it's our responsibility to identify when memory is no longer being used and call code to explicitly return it, just as we did to request it. Doing this correctly has historically been a difficult programming problem. If we forget, we'll waste memory. If we do it too early, we'll have an invalid variable. If we do it twice, that's a bug too. We need to pair exactly one allocate with exactly one free.

Move takes a different path: the memory is automatically returned once the variable that owns it goes out of scope. Here's a version of our scope example using a String instead of a string literal:

module my_module::string_scope {
    public fun string_scope_demo() {
        let s = string::utf8(b"hello");   // s comes into scope

        // do stuff with s
        let len = string::length(&s);
    }   // this scope is now over, and s is no longer valid
}

There is a natural point at which we can return the memory our String needs to the allocator: when s goes out of scope. When a variable goes out of scope, Move calls a special function for us. This function is called drop, and it's where the author of String can put the code to return the memory. Move calls drop automatically at the closing curly bracket.

Ways Variables and Data Interact: Move

Multiple variables can interact with the same data in different ways in Move. Let's look at an example using an integer:

module my_module::move_example {
    public fun move_demo() {
        let x = 5;
        let y = x;
    }
}

We can probably guess what this is doing: "bind the value 5 to x; then make a copy of the value in x and bind it to y." We now have two variables, x and y, and both equal 5. This is indeed what is happening, because integers are simple values with a known, fixed size, and these two 5 values are pushed onto the stack.

Now let's look at the String version:

module my_module::string_move {
    public fun string_move_demo() {
        let s1 = string::utf8(b"hello");
        let s2 = s1;
    }
}

This looks very similar to the previous code, so we might assume that the second line would make a copy of the value in s1 and bind it to s2. But this isn't quite what happens.

To explain what happens, we need to look at what a String looks like under the hood. A String is made up of three parts, shown on the left: a pointer to the memory that holds the contents of the string, a length, and a capacity. This group of data is stored on the stack. On the right is the memory on the heap that holds the contents.

When we assign s1 to s2, the String data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap that the pointer refers to. In other words, the data representation in memory looks like this:

s1: [ptr | len | cap] -> "hello" (on heap)
s2: [ptr | len | cap] -> "hello" (on heap)

The representation is not like this, which is what memory would look like if Move instead copied the heap data as well:

s1: [ptr | len | cap] -> "hello" (on heap)
s2: [ptr | len | cap] -> "hello" (on heap)  // This is NOT what happens

If Move had done this, the operation let s2 = s1; could be very expensive in terms of runtime performance if the data on the heap was large.

Earlier, we said that when a variable goes out of scope, Move automatically calls the drop function and cleans up the heap memory for that variable. But Figure 4-2 shows both data pointers pointing to the same location. This is a problem: when s2 and s1 go out of scope, they will both try to free the same memory. This is known as a double free error and is one of the memory safety bugs we mentioned previously. Freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities.

To ensure memory safety, after the line let s2 = s1;, Move considers s1 as no longer valid. Therefore, Move doesn't need to free anything when s1 goes out of scope. Check out what happens when you try to use s1 after s2 is created; it won't work:

module my_module::invalid_use {
    public fun invalid_use_demo() {
        let s1 = string::utf8(b"hello");
        let s2 = s1;
        
        // This would cause a compilation error:
        // let len = string::length(&s1);
    }
}

If you've heard the terms shallow copy and deep copy while working with other languages, the concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy. But because Move also invalidates the first variable, instead of being called a shallow copy, it's known as a move. In this example, we would say that s1 was moved into s2. So what actually happens is shown in Figure 4-4.

That solves our problem! With only s2 valid, when it goes out of scope, it alone will free the memory, and we're done.

In addition, there's a design choice that's implied by this: Move will never automatically create "deep" copies of your data. Therefore, any automatic copying can be assumed to be inexpensive in terms of runtime performance.

Variables and Data Interacting with Clone

If we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called clone. We'll discuss method syntax in Chapter 5, but because methods are a common feature in many programming languages, you've probably seen them before.

Here's an example of the clone method in action:

module my_module::clone_example {
    public fun clone_demo() {
        let s1 = string::utf8(b"hello");
        let s2 = string::clone(&s1);
        
        // Now both s1 and s2 are valid
        let len1 = string::length(&s1);
        let len2 = string::length(&s2);
    }
}

This works just fine and explicitly produces the behavior shown in Figure 4-3, where the heap data does get copied.

When you see a call to clone, you know that some arbitrary code is being executed and that code may be expensive. It's a visual indicator that something different is going on.

Stack-Only Data: Copy

There's another wrinkle we haven't talked about yet. This code using integers works and is valid:

module my_module::copy_example {
    public fun copy_demo() {
        let x = 5;
        let y = x;
        
        // Both x and y are valid here
        let sum = x + y;
    }
}

But this code seems to contradict what we just learned: we don't have a call to clone, but x is still valid and wasn't moved into y.

The reason is that types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. That means there's no reason we would want to prevent x from being valid after we create the variable y. In other words, there's no difference between deep and shallow copying here, so calling clone wouldn't create anything different from the usual shallow copying, and we can leave it out.

Move has a special annotation called the copy ability that we can place on types like integers that are stored on the stack (we'll talk more about abilities in Chapter 10). If a type has the copy ability, an older variable is still usable after assignment. Move won't let us annotate a type with copy if the type, or any of its parts, has implemented the drop ability. If the type needs something special to happen when the value goes out of scope and we add the copy annotation to that type, we'll get a compile-time error.

So what types have the copy ability? You can check the documentation for the given type to be sure, but as a general rule, any group of simple scalar values can have copy, and nothing that requires allocation or is some form of resource can have copy. Here are some types that have copy:

  • All the integer types, such as u32
  • The Boolean type, bool
  • The address type, address
  • All floating point types, such as f64
  • Tuples, if they only contain types that also have copy. For example, (u32, u64) has copy, but (u32, String) does not

Ownership and Functions

The semantics for passing a value to a function are similar to those for assigning a value to a variable. Passing a variable to a function will move or copy, just as assignment does. Here's an example with some annotations showing where variables go into and out of scope:

module my_module::function_ownership {
    public fun function_ownership_demo() {
        let s = string::utf8(b"hello");  // s comes into scope

        takes_ownership(s);             // s's value moves into the function...
                                        // ... and so is no longer valid here

        let x = 5;                      // x comes into scope

        makes_copy(x);                  // x would move into the function,
                                        // but u64 is Copy, so it's okay to still
                                        // use x afterward
    } // Here, x goes out of scope, then s. But because s's value was moved, nothing
      // special happens.

    fun takes_ownership(some_string: String) { // some_string comes into scope
        let len = string::length(&some_string);
    } // Here, some_string goes out of scope and `drop` is called. The backing
      // memory is freed.

    fun makes_copy(some_integer: u64) { // some_integer comes into scope
        let doubled = some_integer * 2;
    } // Here, some_integer goes out of scope. Nothing special happens.
}

If we tried to use s after the call to takes_ownership, Move would throw a compile-time error. These static checks protect us from mistakes. Try adding code to main that uses s and x to see where you can use them and where the ownership rules prevent you from doing so.

Return Values and Scope

Returning values can also transfer ownership. Here's an example with similar annotations:

module my_module::return_ownership {
    public fun return_ownership_demo() {
        let s1 = gives_ownership();         // gives_ownership moves its return
                                            // value into s1

        let s2 = string::utf8(b"hello");     // s2 comes into scope

        let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                            // takes_and_gives_back, which also
                                            // moves its return value into s3
    } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
      // happens. s1 goes out of scope and is dropped.

    fun gives_ownership() -> String {             // gives_ownership will move its
                                                 // return value into the function
                                                 // that calls it

        let some_string = string::utf8(b"hello"); // some_string comes into scope

        some_string                              // some_string is returned and
                                                 // moves out to the calling
                                                 // function
    }

    // This function takes a String and returns one
    fun takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                          // scope

        a_string  // a_string is returned and moves out to the calling function
    }
}

The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it. When a variable that includes data on the heap goes out of scope, the value will be cleaned up by drop unless ownership of the data has been moved to another variable.

While this works, taking ownership and then returning ownership with every function is a bit tedious. What if we want to let a function use a value but not take ownership? It's quite annoying that anything we pass in also needs to be passed back if we want to use it again, in addition to any data resulting from the body of the function that we might want to return as well.

It's possible to return multiple values using tuples, like this:

module my_module::tuple_return {
    public fun tuple_return_demo() {
        let s1 = string::utf8(b"hello");

        let (s2, len) = calculate_length(s1);
    }

    fun calculate_length(s: String) -> (String, u64) {
        let length = string::length(&s); // len() returns the length of a String

        (s, length) // return both the String and the length
    }
}

But this is too much ceremony and a lot of work for a concept that should be common. Luckily for us, Move has a feature for using a value without transferring ownership, called references.

Move Types and Ownership

Understanding how different Move types interact with ownership is crucial for writing correct and efficient code. This section explores how various types behave with respect to ownership, copying, and moving.

Type Abilities and Ownership

Move's type system is built around abilities that determine how types can be used. These abilities directly affect ownership behavior:

  • copy: The type can be duplicated
  • drop: The type can be destroyed
  • store: The type can be stored in global storage
  • key: The type can be used as a key in global storage

Primitive Types

Primitive types in Move have simple ownership behavior because they are stored on the stack and have known sizes.

Integer Types

All integer types (u8, u16, u32, u64, u128, u256) have the copy and drop abilities:

module my_module::integer_ownership {
    public fun integer_demo() {
        let x: u64 = 42;
        let y = x;  // Copy - x is still valid
        
        // Both x and y are valid
        let sum = x + y;
        
        // Both are automatically dropped at end of function
    }
}

Boolean Type

The bool type has copy and drop abilities:

module my_module::boolean_ownership {
    public fun boolean_demo() {
        let flag = true;
        let flag_copy = flag;  // Copy
        
        // Both flags are valid
        let result = flag && flag_copy;
    }
}

Address Type

The address type has copy and drop abilities:

module my_module::address_ownership {
    public fun address_demo() {
        let addr1 = @0x1;
        let addr2 = addr1;  // Copy
        
        // Both addresses are valid
        let is_equal = addr1 == addr2;
    }
}

Vector Type

The vector type has copy, drop, and store abilities, but its behavior depends on the element type:

module my_module::vector_ownership {
    public fun vector_demo() {
        // Vector of copyable types
        let numbers = vector[1, 2, 3, 4, 5];
        let numbers_copy = numbers;  // Copy - both are valid
        
        // Vector of non-copyable types (like String)
        let strings = vector[
            string::utf8(b"hello"),
            string::utf8(b"world")
        ];
        let strings_moved = strings;  // Move - strings is no longer valid
        
        // This would cause a compilation error:
        // let len = vector::length(&strings);
    }
}

String Type

The String type has copy, drop, and store abilities, but copying can be expensive:

module my_module::string_ownership {
    public fun string_demo() {
        let s1 = string::utf8(b"hello");
        let s2 = s1;  // Move - s1 is no longer valid
        
        // This would cause a compilation error:
        // let len = string::length(&s1);
        
        // s2 is still valid
        let len = string::length(&s2);
    }
    
    public fun string_copy_demo() {
        let s1 = string::utf8(b"hello");
        let s2 = string::clone(&s1);  // Explicit copy - both are valid
        
        // Both strings are valid
        let len1 = string::length(&s1);
        let len2 = string::length(&s2);
    }
}

Struct Types

Struct ownership behavior depends on the abilities declared for the struct:

Copyable Structs

module my_module::copyable_struct {
    struct Point has copy, drop, store {
        x: u64,
        y: u64,
    }
    
    public fun copyable_demo() {
        let p1 = Point { x: 10, y: 20 };
        let p2 = p1;  // Copy - p1 is still valid
        
        // Both points are valid
        let sum_x = p1.x + p2.x;
        let sum_y = p1.y + p2.y;
    }
}

Non-Copyable Structs

module my_module::non_copyable_struct {
    struct Message has drop, store {
        content: String,
        timestamp: u64,
    }
    
    public fun non_copyable_demo() {
        let msg1 = Message {
            content: string::utf8(b"Hello"),
            timestamp: 1234567890,
        };
        let msg2 = msg1;  // Move - msg1 is no longer valid
        
        // This would cause a compilation error:
        // let content = msg1.content;
        
        // msg2 is still valid
        let content = msg2.content;
    }
}

Resource Structs

Resources are special structs that cannot be copied or dropped:

module my_module::resource_struct {
    struct Coin has key, store {
        value: u64,
    }
    
    public fun resource_demo() {
        let coin = Coin { value: 100 };
        
        // Resources cannot be copied
        // let coin_copy = coin;  // This would cause a compilation error
        
        // Resources cannot be dropped automatically
        // They must be explicitly moved to global storage or returned
        
        // Move to global storage
        // move_to(account, coin);
        
        // Or return the resource
        coin
    }
}

Tuple Types

Tuple ownership behavior depends on the types of its elements:

module my_module::tuple_ownership {
    public fun tuple_demo() {
        // Tuple with all copyable elements
        let t1 = (42, true, @0x1);
        let t2 = t1;  // Copy - t1 is still valid
        
        // Tuple with non-copyable elements
        let t3 = (string::utf8(b"hello"), 42);
        let t4 = t3;  // Move - t3 is no longer valid
        
        // This would cause a compilation error:
        // let (s, n) = t3;
        
        // t4 is still valid
        let (s, n) = t4;
    }
}

Reference Types

References provide controlled access to values without transferring ownership:

Immutable References

module my_module::immutable_references {
    public fun immutable_ref_demo() {
        let s = string::utf8(b"hello");
        let len = calculate_length(&s);  // Pass immutable reference
        
        // s is still valid
        let is_empty = string::is_empty(&s);
    }
    
    fun calculate_length(s: &String): u64 {
        string::length(s)  // Can read but not modify
    }
}

Mutable References

module my_module::mutable_references {
    public fun mutable_ref_demo() {
        let mut s = string::utf8(b"hello");
        append_world(&mut s);  // Pass mutable reference
        
        // s is still valid and has been modified
        let len = string::length(&s);
    }
    
    fun append_world(s: &mut String) {
        *s = *s + string::utf8(b" world");  // Can modify the value
    }
}

Global Storage and Ownership

Global storage in Move has special ownership rules:

Storing Resources

module my_module::global_storage {
    struct UserProfile has key, store {
        name: String,
        age: u8,
    }
    
    public fun store_profile(account: &signer, name: String, age: u8) {
        let profile = UserProfile { name, age };
        move_to(account, profile);  // Transfer ownership to global storage
    }
    
    public fun get_profile_name(addr: address): String {
        let profile = borrow_global<UserProfile>(addr);
        profile.name  // Return a copy of the name
    }
    
    public fun update_profile_age(addr: address, new_age: u8) {
        let profile = borrow_global_mut<UserProfile>(addr);
        profile.age = new_age;  // Modify the stored value
    }
}

Moving Resources Out of Storage

module my_module::move_from_storage {
    struct Coin has key, store {
        value: u64,
    }
    
    public fun withdraw_coin(account: &signer, amount: u64): Coin {
        let coin = move_from<Coin>(signer::address_of(account));
        
        // Split the coin if needed
        if (coin.value > amount) {
            let (withdrawn, remaining) = split_coin(coin, amount);
            // Return the remaining coin to storage
            move_to(account, remaining);
            withdrawn
        } else {
            coin
        }
    }
    
    fun split_coin(coin: Coin, amount: u64): (Coin, Coin) {
        // Implementation of coin splitting
        // This is a simplified example
        (Coin { value: amount }, Coin { value: coin.value - amount })
    }
}

Type Abilities and Constraints

Understanding abilities helps you design types with the right ownership behavior:

Designing Copyable Types

module my_module::copyable_design {
    // Good: All fields are copyable
    struct Config has copy, drop, store {
        max_retries: u8,
        timeout: u64,
        enabled: bool,
    }
    
    // Bad: Contains non-copyable field
    // struct BadConfig has copy, drop, store {
    //     max_retries: u8,
    //     name: String,  // String doesn't have copy ability
    // }
}

Designing Resource Types

module my_module::resource_design {
    // Good: Resource with key and store abilities
    struct Token has key, store {
        id: u64,
        owner: address,
        metadata: String,
    }
    
    // Bad: Resource with copy ability (contradiction)
    // struct BadToken has key, store, copy {
    //     id: u64,
    //     owner: address,
    // }
}

Ownership Patterns

Value Semantics

module my_module::value_semantics {
    struct Point has copy, drop, store {
        x: u64,
        y: u64,
    }
    
    public fun value_demo() {
        let p1 = Point { x: 10, y: 20 };
        let p2 = p1;  // Copy - value semantics
        
        // Modifying p2 doesn't affect p1
        p2.x = 30;
        
        // p1.x is still 10
        let x1 = p1.x;
        let x2 = p2.x;  // 30
    }
}

Reference Semantics

module my_module::reference_semantics {
    struct Counter has key, store {
        value: u64,
    }
    
    public fun reference_demo(account: &signer) {
        let counter = Counter { value: 0 };
        move_to(account, counter);
        
        // All operations work on the same instance
        increment_counter(signer::address_of(account));
        increment_counter(signer::address_of(account));
        
        let final_value = get_counter_value(signer::address_of(account));
        // final_value is 2
    }
    
    fun increment_counter(addr: address) {
        let counter = borrow_global_mut<Counter>(addr);
        counter.value = counter.value + 1;
    }
    
    fun get_counter_value(addr: address): u64 {
        let counter = borrow_global<Counter>(addr);
        counter.value
    }
}

Best Practices

Choose Appropriate Abilities

module my_module::ability_best_practices {
    // Use copy for small, simple data
    struct SmallData has copy, drop, store {
        id: u64,
        flag: bool,
    }
    
    // Use drop for temporary data
    struct TempData has drop {
        buffer: vector<u8>,
    }
    
    // Use store for data that needs persistence
    struct PersistentData has key, store {
        data: String,
    }
    
    // Use key for resources
    struct Resource has key, store {
        value: u64,
    }
}

Avoid Unnecessary Copies

module my_module::avoid_copies {
    public fun efficient_processing(data: &vector<u64>): u64 {
        let sum = 0u64;
        let i = 0;
        let len = vector::length(data);
        
        while (i < len) {
            sum = sum + *vector::borrow(data, i);
            i = i + 1;
        };
        
        sum
    }
    
    // Less efficient - creates unnecessary copies
    // public fun inefficient_processing(data: vector<u64>): u64 {
    //     // data is moved into the function
    //     // and would need to be returned if needed elsewhere
    // }
}

Use References for Read-Only Access

module my_module::reference_best_practices {
    public fun read_only_operations(data: &String): (u64, bool) {
        let length = string::length(data);
        let is_empty = string::is_empty(data);
        (length, is_empty)
    }
    
    public fun modify_data(data: &mut String) {
        *data = *data + string::utf8(b" modified");
    }
}

Understanding how different types interact with ownership is essential for writing efficient and correct Move code. The ability system provides a clear way to express ownership semantics, and following best practices helps you avoid common pitfalls.

Transfer and Borrowing

Understanding how to transfer ownership and borrow values is essential for writing efficient Move code. This section covers the mechanics of ownership transfer and the borrowing system that allows controlled access to values without transferring ownership.

Ownership Transfer

Ownership transfer occurs when a value is moved from one owner to another. This is the default behavior in Move for most types.

Basic Ownership Transfer

module my_module::basic_transfer {
    public fun basic_transfer_demo() {
        let s1 = string::utf8(b"hello");
        let s2 = s1;  // Ownership transferred from s1 to s2
        
        // s1 is no longer valid
        // s2 is now the owner
        
        let len = string::length(&s2);
    } // s2 goes out of scope and is dropped
}

Transfer in Function Calls

module my_module::function_transfer {
    public fun function_transfer_demo() {
        let s = string::utf8(b"hello");
        
        // Ownership transferred to the function
        takes_ownership(s);
        
        // s is no longer valid here
        // This would cause a compilation error:
        // let len = string::length(&s);
    }
    
    fun takes_ownership(some_string: String) {
        // some_string is now owned by this function
        let len = string::length(&some_string);
    } // some_string goes out of scope and is dropped
}

Transfer with Return Values

module my_module::return_transfer {
    public fun return_transfer_demo() {
        let s1 = gives_ownership();  // Ownership transferred from function to s1
        
        let s2 = string::utf8(b"world");
        let s3 = takes_and_gives_back(s2);  // s2 moved in, s3 moved out
    }
    
    fun gives_ownership() -> String {
        let some_string = string::utf8(b"hello");
        some_string  // Ownership transferred to caller
    }
    
    fun takes_and_gives_back(a_string: String) -> String {
        // a_string comes into scope
        a_string  // Ownership transferred back to caller
    }
}

Transfer with Structs

module my_module::struct_transfer {
    struct Person has drop, store {
        name: String,
        age: u8,
    }
    
    public fun struct_transfer_demo() {
        let person1 = Person {
            name: string::utf8(b"Alice"),
            age: 30,
        };
        
        let person2 = person1;  // Ownership transferred
        
        // person1 is no longer valid
        // person2 is now the owner
        
        let age = person2.age;
    }
}

Borrowing

Borrowing allows you to use a value without taking ownership of it. Borrowing is done using references (& for immutable references, &mut for mutable references).

Immutable Borrowing

module my_module::immutable_borrowing {
    public fun immutable_borrow_demo() {
        let s = string::utf8(b"hello");
        
        // Borrow s immutably
        let len = calculate_length(&s);
        let is_empty = string::is_empty(&s);
        
        // s is still valid and can be used
        let first_char = string::sub_string(&s, 0, 1);
    }
    
    fun calculate_length(s: &String) -> u64 {
        string::length(s)  // Can read but not modify
    }
}

Mutable Borrowing

module my_module::mutable_borrowing {
    public fun mutable_borrow_demo() {
        let mut s = string::utf8(b"hello");
        
        // Borrow s mutably
        change_string(&mut s);
        
        // s is still valid and has been modified
        let len = string::length(&s);
    }
    
    fun change_string(s: &mut String) {
        *s = *s + string::utf8(b" world");  // Can modify the value
    }
}

Multiple Immutable Borrows

module my_module::multiple_immutable_borrows {
    public fun multiple_immutable_demo() {
        let s = string::utf8(b"hello");
        
        // Multiple immutable borrows are allowed
        let len1 = string::length(&s);
        let len2 = string::length(&s);
        let is_empty = string::is_empty(&s);
        
        // All borrows can be used simultaneously
        let result = len1 + len2;
    }
}

Mutable and Immutable Borrows

module my_module::mutable_immutable_conflict {
    public fun conflict_demo() {
        let mut s = string::utf8(b"hello");
        
        let len = string::length(&s);  // Immutable borrow
        
        // This would cause a compilation error:
        // change_string(&mut s);  // Mutable borrow while immutable borrow exists
        
        // But this is fine:
        let is_empty = string::is_empty(&s);  // Another immutable borrow
    }
    
    fun change_string(s: &mut String) {
        *s = *s + string::utf8(b" world");
    }
}

Borrowing Rules

Move enforces strict borrowing rules to prevent data races and ensure memory safety:

  1. At any given time, you can have either one mutable reference or any number of immutable references to a particular value
  2. References must always be valid

Borrowing Rule Examples

module my_module::borrowing_rules {
    public fun borrowing_rules_demo() {
        let mut s = string::utf8(b"hello");
        
        // Rule 1: Multiple immutable borrows allowed
        let r1 = &s;
        let r2 = &s;
        let r3 = &s;
        
        // All immutable borrows can be used
        let len1 = string::length(r1);
        let len2 = string::length(r2);
        let len3 = string::length(r3);
        
        // Rule 2: Cannot have mutable borrow while immutable borrows exist
        // This would cause a compilation error:
        // let r4 = &mut s;
        
        // Rule 3: Cannot have multiple mutable borrows
        // let r5 = &mut s;
        // let r6 = &mut s;  // This would cause a compilation error
    }
}

Dangling References

Move prevents dangling references (references to data that no longer exists):

module my_module::dangling_references {
    // This would cause a compilation error - dangling reference
    // fun dangle() -> &String {
    //     let s = string::utf8(b"hello");
    //     &s  // s goes out of scope, so this reference would be invalid
    // }
    
    // Instead, return the value itself
    fun no_dangle() -> String {
        let s = string::utf8(b"hello");
        s  // Ownership transferred to caller
    }
}

Borrowing in Structs

Structs can contain references, but they must follow the borrowing rules:

module my_module::struct_borrowing {
    struct StringWrapper has drop, store {
        content: String,
    }
    
    public fun struct_borrow_demo() {
        let wrapper = StringWrapper {
            content: string::utf8(b"hello"),
        };
        
        // Borrow the struct immutably
        let len = get_length(&wrapper);
        
        // Borrow the struct mutably
        append_world(&mut wrapper);
        
        // Both operations work on the same struct
        let final_len = string::length(&wrapper.content);
    }
    
    fun get_length(wrapper: &StringWrapper) -> u64 {
        string::length(&wrapper.content)
    }
    
    fun append_world(wrapper: &mut StringWrapper) {
        wrapper.content = wrapper.content + string::utf8(b" world");
    }
}

Borrowing with Collections

Borrowing works with collections like vectors:

module my_module::collection_borrowing {
    public fun vector_borrow_demo() {
        let mut numbers = vector[1, 2, 3, 4, 5];
        
        // Immutable borrow
        let sum = calculate_sum(&numbers);
        
        // Mutable borrow
        double_values(&mut numbers);
        
        // Both operations work on the same vector
        let new_sum = calculate_sum(&numbers);
    }
    
    fun calculate_sum(numbers: &vector<u64>) -> u64 {
        let sum = 0u64;
        let i = 0;
        let len = vector::length(numbers);
        
        while (i < len) {
            sum = sum + *vector::borrow(numbers, i);
            i = i + 1;
        };
        
        sum
    }
    
    fun double_values(numbers: &mut vector<u64>) {
        let i = 0;
        let len = vector::length(numbers);
        
        while (i < len) {
            let value = *vector::borrow(numbers, i);
            *vector::borrow_mut(numbers, i) = value * 2;
            i = i + 1;
        };
    }
}

Borrowing in Global Storage

Borrowing is essential for working with global storage:

module my_module::global_storage_borrowing {
    struct Counter has key, store {
        value: u64,
    }
    
    public fun global_borrow_demo(account: &signer) {
        let counter = Counter { value: 0 };
        move_to(account, counter);
        
        let addr = signer::address_of(account);
        
        // Immutable borrow from global storage
        let current_value = get_counter_value(addr);
        
        // Mutable borrow from global storage
        increment_counter(addr);
        
        // Verify the change
        let new_value = get_counter_value(addr);
    }
    
    fun get_counter_value(addr: address) -> u64 {
        let counter = borrow_global<Counter>(addr);
        counter.value
    }
    
    fun increment_counter(addr: address) {
        let counter = borrow_global_mut<Counter>(addr);
        counter.value = counter.value + 1;
    }
}

Borrowing Patterns

Read-Only Operations

module my_module::read_only_pattern {
    public fun read_only_demo(data: &vector<u64>) -> (u64, u64, bool) {
        let sum = calculate_sum(data);
        let average = calculate_average(data);
        let is_empty = vector::is_empty(data);
        (sum, average, is_empty)
    }
    
    fun calculate_sum(data: &vector<u64>) -> u64 {
        let sum = 0u64;
        let i = 0;
        let len = vector::length(data);
        
        while (i < len) {
            sum = sum + *vector::borrow(data, i);
            i = i + 1;
        };
        
        sum
    }
    
    fun calculate_average(data: &vector<u64>) -> u64 {
        let sum = calculate_sum(data);
        let len = vector::length(data);
        if (len == 0) {
            0
        } else {
            sum / len
        }
    }
}

Modification Operations

module my_module::modification_pattern {
    public fun modification_demo(data: &mut vector<u64>) {
        // Sort the data
        sort_vector(data);
        
        // Remove duplicates
        remove_duplicates(data);
        
        // Double all values
        double_all(data);
    }
    
    fun sort_vector(data: &mut vector<u64>) {
        // Implementation of sorting
        // This is a simplified example
    }
    
    fun remove_duplicates(data: &mut vector<u64>) {
        // Implementation of duplicate removal
        // This is a simplified example
    }
    
    fun double_all(data: &mut vector<u64>) {
        let i = 0;
        let len = vector::length(data);
        
        while (i < len) {
            let value = *vector::borrow(data, i);
            *vector::borrow_mut(data, i) = value * 2;
            i = i + 1;
        };
    }
}

Best Practices

Prefer Borrowing Over Transfer

module my_module::borrowing_best_practices {
    // Good: Use borrowing for read-only access
    public fun efficient_processing(data: &vector<u64>) -> u64 {
        let sum = 0u64;
        let i = 0;
        let len = vector::length(data);
        
        while (i < len) {
            sum = sum + *vector::borrow(data, i);
            i = i + 1;
        };
        
        sum
    }
    
    // Less efficient: Transfer ownership
    // public fun inefficient_processing(data: vector<u64>) -> (vector<u64>, u64) {
    //     let sum = calculate_sum(&data);
    //     (data, sum)  // Must return data to avoid moving
    // }
}

Use Appropriate Reference Types

module my_module::reference_types {
    // Use immutable references for read-only access
    public fun read_operations(data: &String) -> (u64, bool) {
        let length = string::length(data);
        let is_empty = string::is_empty(data);
        (length, is_empty)
    }
    
    // Use mutable references only when modification is needed
    public fun modification_operations(data: &mut String) {
        *data = *data + string::utf8(b" modified");
    }
}

Avoid Borrowing Conflicts

module my_module::avoid_conflicts {
    public fun avoid_conflict_demo() {
        let mut data = vector[1, 2, 3, 4, 5];
        
        // Good: Use immutable borrows first
        let sum = calculate_sum(&data);
        let average = calculate_average(&data);
        
        // Then use mutable borrow
        modify_data(&mut data);
        
        // Now can use immutable borrows again
        let new_sum = calculate_sum(&data);
    }
    
    fun calculate_sum(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun calculate_average(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun modify_data(data: &mut vector<u64>) {
        // Implementation
    }
}

Understanding ownership transfer and borrowing is fundamental to writing safe and efficient Move code. The borrowing system provides a powerful way to control access to data while maintaining Move's safety guarantees.

Reference Restrictions

Move's reference system includes strict restrictions that prevent common programming errors like data races, dangling references, and use-after-free bugs. Understanding these restrictions is crucial for writing safe Move code.

The Borrowing Rules

Move enforces three fundamental borrowing rules that must be followed at all times:

  1. At any given time, you can have either one mutable reference or any number of immutable references to a particular value
  2. References must always be valid
  3. References cannot outlive the data they refer to

Rule 1: Mutable vs Immutable References

This rule prevents data races by ensuring that data cannot be modified while it's being read.

Multiple Immutable References Allowed

module my_module::multiple_immutable {
    public fun multiple_immutable_demo() {
        let s = string::utf8(b"hello");
        
        // Multiple immutable references are allowed
        let r1 = &s;
        let r2 = &s;
        let r3 = &s;
        
        // All can be used simultaneously
        let len1 = string::length(r1);
        let len2 = string::length(r2);
        let len3 = string::length(r3);
        
        // This is safe because no one can modify the data
        let total = len1 + len2 + len3;
    }
}

Only One Mutable Reference

module my_module::single_mutable {
    public fun single_mutable_demo() {
        let mut s = string::utf8(b"hello");
        
        // Only one mutable reference allowed
        let r1 = &mut s;
        
        // This would cause a compilation error:
        // let r2 = &mut s;  // Second mutable reference
        
        // Use the mutable reference
        *r1 = *r1 + string::utf8(b" world");
    }
}

Mutable and Immutable Reference Conflict

module my_module::mutable_immutable_conflict {
    public fun conflict_demo() {
        let mut s = string::utf8(b"hello");
        
        // Create immutable reference
        let r1 = &s;
        let len = string::length(r1);
        
        // This would cause a compilation error:
        // let r2 = &mut s;  // Mutable reference while immutable exists
        
        // But this is fine - another immutable reference
        let r3 = &s;
        let is_empty = string::is_empty(r3);
    }
}

Rule 2: References Must Always Be Valid

Move prevents dangling references by ensuring references always point to valid data.

Preventing Dangling References

module my_module::dangling_prevention {
    // This would cause a compilation error - dangling reference
    // fun dangle() -> &String {
    //     let s = string::utf8(b"hello");
    //     &s  // s goes out of scope, making this reference invalid
    // }
    
    // Instead, return the value itself
    fun no_dangle() -> String {
        let s = string::utf8(b"hello");
        s  // Ownership transferred to caller
    }
    
    // Or return a reference to data that lives long enough
    struct StringHolder has key, store {
        content: String,
    }
    
    fun get_reference(holder: &StringHolder) -> &String {
        &holder.content  // Reference to data that outlives the function
    }
}

Reference Lifetime in Structs

module my_module::struct_lifetime {
    // This would cause a compilation error - reference in struct
    // struct StringRef {
    //     content: &String,  // Reference without lifetime
    // }
    
    // Instead, store the value itself
    struct StringWrapper has drop, store {
        content: String,
    }
    
    // Or use a different design pattern
    struct StringHolder has key, store {
        content: String,
    }
    
    fun create_wrapper(s: String) -> StringWrapper {
        StringWrapper { content: s }
    }
}

Rule 3: References Cannot Outlive Data

This rule ensures that references are always valid throughout their lifetime.

Scope and Lifetime

module my_module::scope_lifetime {
    public fun scope_demo() {
        let mut s = string::utf8(b"hello");
        
        {
            let r = &mut s;  // Mutable reference to s
            *r = *r + string::utf8(b" world");
        } // r goes out of scope here
        
        // s is still valid and can be used
        let len = string::length(&s);
    }
}

Function Parameter Lifetimes

module my_module::function_lifetime {
    public fun function_demo() {
        let s = string::utf8(b"hello");
        
        // Pass reference to function
        let len = calculate_length(&s);
        
        // s is still valid after the function call
        let is_empty = string::is_empty(&s);
    }
    
    fun calculate_length(s: &String) -> u64 {
        string::length(s)  // Reference is valid for the duration of the function
    }
}

Reference Restrictions in Practice

Working with Collections

module my_module::collection_restrictions {
    public fun vector_restrictions() {
        let mut numbers = vector[1, 2, 3, 4, 5];
        
        // Immutable borrow
        let sum = calculate_sum(&numbers);
        
        // Mutable borrow
        double_values(&mut numbers);
        
        // Can use immutable borrow again after mutable borrow ends
        let new_sum = calculate_sum(&numbers);
    }
    
    fun calculate_sum(numbers: &vector<u64>) -> u64 {
        let sum = 0u64;
        let i = 0;
        let len = vector::length(numbers);
        
        while (i < len) {
            sum = sum + *vector::borrow(numbers, i);
            i = i + 1;
        };
        
        sum
    }
    
    fun double_values(numbers: &mut vector<u64>) {
        let i = 0;
        let len = vector::length(numbers);
        
        while (i < len) {
            let value = *vector::borrow(numbers, i);
            *vector::borrow_mut(numbers, i) = value * 2;
            i = i + 1;
        };
    }
}

Global Storage Restrictions

module my_module::global_storage_restrictions {
    struct Counter has key, store {
        value: u64,
    }
    
    public fun global_storage_demo(account: &signer) {
        let counter = Counter { value: 0 };
        move_to(account, counter);
        
        let addr = signer::address_of(account);
        
        // Immutable borrow from global storage
        let current_value = get_counter_value(addr);
        
        // Mutable borrow from global storage
        increment_counter(addr);
        
        // Can use immutable borrow again
        let new_value = get_counter_value(addr);
    }
    
    fun get_counter_value(addr: address) -> u64 {
        let counter = borrow_global<Counter>(addr);
        counter.value  // Return copy of value, not reference
    }
    
    fun increment_counter(addr: address) {
        let counter = borrow_global_mut<Counter>(addr);
        counter.value = counter.value + 1;
    }
}

Common Reference Errors and Solutions

Error: Multiple Mutable References

module my_module::multiple_mutable_error {
    public fun multiple_mutable_error() {
        let mut s = string::utf8(b"hello");
        
        // This would cause a compilation error:
        // let r1 = &mut s;
        // let r2 = &mut s;  // Second mutable reference
        
        // Solution: Use one mutable reference at a time
        {
            let r1 = &mut s;
            *r1 = *r1 + string::utf8(b" world");
        } // r1 goes out of scope
        
        {
            let r2 = &mut s;
            *r2 = *r2 + string::utf8(b"!");
        } // r2 goes out of scope
    }
}

Error: Mutable and Immutable Reference Conflict

module my_module::conflict_error {
    public fun conflict_error() {
        let mut s = string::utf8(b"hello");
        
        // This would cause a compilation error:
        // let r1 = &s;  // Immutable reference
        // let r2 = &mut s;  // Mutable reference while immutable exists
        
        // Solution: Use immutable references first, then mutable
        let len = string::length(&s);
        let is_empty = string::is_empty(&s);
        
        // Now use mutable reference
        let r2 = &mut s;
        *r2 = *r2 + string::utf8(b" world");
    }
}

Error: Dangling Reference

module my_module::dangling_error {
    // This would cause a compilation error:
    // fun dangling_reference() -> &String {
    //     let s = string::utf8(b"hello");
    //     &s  // s goes out of scope, making reference invalid
    // }
    
    // Solution: Return the value itself
    fun return_value() -> String {
        let s = string::utf8(b"hello");
        s  // Ownership transferred
    }
    
    // Or use a different design pattern
    struct StringHolder has key, store {
        content: String,
    }
    
    fun return_reference_to_stored_data(addr: address) -> &String {
        let holder = borrow_global<StringHolder>(addr);
        &holder.content  // Reference to data that lives in global storage
    }
}

Reference Restrictions and Performance

Avoiding Unnecessary Copies

module my_module::performance_restrictions {
    // Good: Use references to avoid copying
    public fun efficient_processing(data: &vector<u64>) -> u64 {
        let sum = 0u64;
        let i = 0;
        let len = vector::length(data);
        
        while (i < len) {
            sum = sum + *vector::borrow(data, i);
            i = i + 1;
        };
        
        sum
    }
    
    // Less efficient: Copy the entire vector
    // public fun inefficient_processing(data: vector<u64>) -> u64 {
    //     // data is moved into the function
    //     // processing happens
    //     // data would need to be returned if needed elsewhere
    // }
}

Minimizing Borrowing Conflicts

module my_module::minimize_conflicts {
    public fun minimize_conflicts_demo() {
        let mut data = vector[1, 2, 3, 4, 5];
        
        // Group immutable operations together
        let sum = calculate_sum(&data);
        let average = calculate_average(&data);
        let max = find_max(&data);
        
        // Then do mutable operations
        modify_data(&mut data);
        
        // Can use immutable operations again
        let new_sum = calculate_sum(&data);
    }
    
    fun calculate_sum(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun calculate_average(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun find_max(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun modify_data(data: &mut vector<u64>) {
        // Implementation
    }
}

Best Practices for Reference Restrictions

Plan Your Borrowing Strategy

module my_module::borrowing_strategy {
    public fun borrowing_strategy_demo() {
        let mut data = vector[1, 2, 3, 4, 5];
        
        // Strategy 1: Read-only operations first
        let analysis = analyze_data(&data);
        
        // Strategy 2: Single mutable operation
        modify_data(&mut data);
        
        // Strategy 3: Read-only operations after modification
        let new_analysis = analyze_data(&data);
    }
    
    fun analyze_data(data: &vector<u64>) -> (u64, u64, u64) {
        let sum = calculate_sum(data);
        let average = calculate_average(data);
        let max = find_max(data);
        (sum, average, max)
    }
    
    fun modify_data(data: &mut vector<u64>) {
        // Single modification operation
        double_all_values(data);
    }
    
    fun calculate_sum(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun calculate_average(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun find_max(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun double_all_values(data: &mut vector<u64>) {
        // Implementation
    }
}

Use Appropriate Reference Types

module my_module::appropriate_references {
    // Use immutable references for read-only access
    public fun read_operations(data: &String) -> (u64, bool) {
        let length = string::length(data);
        let is_empty = string::is_empty(data);
        (length, is_empty)
    }
    
    // Use mutable references only when modification is needed
    public fun modification_operations(data: &mut String) {
        *data = *data + string::utf8(b" modified");
    }
    
    // Avoid unnecessary mutable references
    // public fun unnecessary_mutable(data: &mut String) -> u64 {
    //     string::length(data)  // Doesn't need mutable reference
    // }
}

Handle Borrowing Conflicts Gracefully

module my_module::graceful_conflicts {
    public fun graceful_conflict_demo() {
        let mut data = vector[1, 2, 3, 4, 5];
        
        // Extract values before modification to avoid conflicts
        let current_sum = calculate_sum(&data);
        let current_average = calculate_average(&data);
        
        // Now modify the data
        modify_data(&mut data);
        
        // Use the extracted values
        let new_sum = calculate_sum(&data);
        let sum_difference = new_sum - current_sum;
    }
    
    fun calculate_sum(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun calculate_average(data: &vector<u64>) -> u64 {
        // Implementation
        0
    }
    
    fun modify_data(data: &mut vector<u64>) {
        // Implementation
    }
}

Understanding and following Move's reference restrictions is essential for writing safe and correct code. These restrictions prevent common programming errors and ensure that your Move programs are memory-safe and free from data races.

Using Structs to Structure Related Data

A struct is a custom data type that lets you package together and name multiple related values that make up a meaningful group. If you're familiar with an object-oriented language, a struct is like the data attributes of an object. In this chapter, we'll compare and contrast structs, demonstrate how to use them, and discuss how structs in Move become powerful resources that represent digital assets.

Why Structs?

When building smart contracts, you frequently need to group related data together. For example, a user profile might include a name, age, and email address. Rather than managing these as separate variables, a struct lets you combine them into a single, named type.

Move structs are more powerful than structures in many other languages because of the abilities system. By assigning different abilities to a struct, you control whether it can be:

  • Stored in global storage (key)
  • Embedded within other structures (store)
  • Copied (copy)
  • Automatically destroyed (drop)

This fine-grained control is what makes Move's type system uniquely suited for representing digital assets on the blockchain.

What You'll Learn

In this chapter, we'll cover:

  1. Defining and Instantiating Structs - How to declare structs and create instances
  2. Structs and How They Become Resources - How abilities transform structs into resources
  3. Structs and Abilities - A deep dive into the four abilities
  4. Example Program Using Structs - A practical, end-to-end example
  5. Struct Method Syntax - How to define methods on structs

Defining and Instantiating Structs

Structs are the primary way to create custom data types in Move. They group related fields together under a single name, much like structs in Rust or C.

Defining a Struct

A struct is defined using the struct keyword followed by the struct name, optional abilities, and the fields inside curly braces.

module my_addr::profiles {
    use std::string::String;

    struct UserProfile has key, store {
        name: String,
        age: u8,
        balance: u64,
    }
}

Key points about struct definitions:

  • The struct name must start with an uppercase letter by convention.
  • Each field has a name and a type, separated by a colon.
  • Fields are separated by commas.
  • Abilities (key, store, copy, drop) are declared after has.

Creating Struct Instances

You create a struct instance by specifying the struct name and providing values for each field.

module my_addr::profiles {
    use std::string::{Self, String};

    struct UserProfile has key, store, drop {
        name: String,
        age: u8,
        balance: u64,
    }

    public fun create_profile(name: String, age: u8): UserProfile {
        UserProfile {
            name,
            age,
            balance: 0,
        }
    }
}

Field Init Shorthand

When a variable has the same name as the struct field, you can use the shorthand syntax. In the example above, name and age are used directly without writing name: name and age: age.

Accessing Struct Fields

You access struct fields using dot notation:

public fun get_name(profile: &UserProfile): &String {
    &profile.name
}

public fun get_age(profile: &UserProfile): u8 {
    profile.age
}

Modifying Struct Fields

To modify fields, you need a mutable reference to the struct:

public fun update_age(profile: &mut UserProfile, new_age: u8) {
    profile.age = new_age;
}

public fun add_balance(profile: &mut UserProfile, amount: u64) {
    profile.balance = profile.balance + amount;
}

Destructuring Structs

You can destructure a struct to extract its fields. This is required when you want to destroy a struct that does not have the drop ability.

public fun destroy_profile(profile: UserProfile): (String, u8, u64) {
    let UserProfile { name, age, balance } = profile;
    (name, age, balance)
}

You can also partially destructure with .. to ignore fields:

public fun get_balance_from_profile(profile: UserProfile): u64 {
    let UserProfile { balance, .. } = profile;
    balance
}

Storing Structs in Global Storage

Structs with the key ability can be stored in global storage under an account address:

module my_addr::profiles {
    use std::signer;
    use std::string::String;

    struct UserProfile has key, store {
        name: String,
        age: u8,
        balance: u64,
    }

    /// Store a profile under the caller's account
    public entry fun register(account: &signer, name: String, age: u8) {
        let profile = UserProfile {
            name,
            age,
            balance: 0,
        };
        move_to(account, profile);
    }

    /// Read the profile for a given address
    #[view]
    public fun get_profile_age(addr: address): u8 acquires UserProfile {
        UserProfile[addr].age
    }
}

Nested Structs

Structs can contain other structs as fields:

module my_addr::game {
    struct Position has copy, drop, store {
        x: u64,
        y: u64,
    }

    struct Player has key, store {
        position: Position,
        health: u64,
        score: u64,
    }
}

Best Practices

  1. Use descriptive field names: Make field names clear and self-documenting.
  2. Minimize abilities: Only add the abilities your struct actually needs.
  3. Group related data: If several values are always used together, put them in a struct.
  4. Document with ///: Add doc comments above your struct definitions explaining their purpose.
  5. Use key for top-level storage: Only structs that need to be stored directly under an address need key.

Structs and How They Become Resources

One of the most powerful concepts in Move is the idea of a resource -- a struct whose usage is tightly controlled by the type system to prevent duplication or accidental loss. Resources are the foundation of safe digital asset management on Aptos.

What is a Resource?

A resource is any struct that has the key ability but lacks the copy ability. Because a resource cannot be copied, it can only exist in one place at a time, which makes it ideal for representing assets like tokens, NFTs, or game items.

module my_addr::vault {
    /// A vault that holds a balance. Because it has `key` but not `copy`,
    /// it is a resource -- it cannot be duplicated.
    struct Vault has key {
        balance: u64,
    }
}

Resources vs. Regular Structs

PropertyRegular Struct (copy + drop)Resource (key, no copy)
Can be copiedYesNo
Can be implicitly droppedYes (with drop)No
Stored in global storageOnly with keyYes
Ideal forTemporary data, configAssets, tokens, game items

Creating Resources

Resources are created like any other struct, but they must be explicitly stored or returned -- they cannot simply go out of scope.

module my_addr::vault {
    use std::signer;

    struct Vault has key {
        balance: u64,
    }

    /// Create a vault and store it under the caller's account
    public entry fun create_vault(account: &signer, initial_balance: u64) {
        let vault = Vault { balance: initial_balance };
        move_to(account, vault);
    }
}

Global Storage Operations

Move provides four built-in operations for working with resources in global storage:

move_to -- Store a resource

Stores a resource under a signer's address. Each account can hold at most one instance of a given resource type.

move_to(account, Vault { balance: 100 });

move_from -- Remove a resource

Removes and returns a resource from global storage. This transfers ownership back to the caller.

let vault = move_from<Vault>(addr);

borrow_global -- Immutable reference

Borrows an immutable reference to a resource in global storage.

let vault_ref = borrow_global<Vault>(addr);
let balance = vault_ref.balance;

borrow_global_mut -- Mutable reference

Borrows a mutable reference to a resource in global storage.

let vault_mut = borrow_global_mut<Vault>(addr);
vault_mut.balance = vault_mut.balance + 100;

Note: The newer indexing syntax Vault[addr] is equivalent to borrow_global<Vault>(addr) and &mut Vault[addr] is equivalent to borrow_global_mut<Vault>(addr).

exists -- Check existence

Checks whether a resource exists at a given address.

if (exists<Vault>(addr)) {
    // The vault exists
};

The acquires Annotation

Any function that accesses global storage for a resource type must declare this with the acquires keyword:

#[view]
public fun get_balance(addr: address): u64 acquires Vault {
    Vault[addr].balance
}

public entry fun deposit(account: &signer, amount: u64) acquires Vault {
    let addr = signer::address_of(account);
    let vault = &mut Vault[addr];
    vault.balance = vault.balance + amount;
}

Destroying Resources

Because resources cannot be implicitly dropped, you must explicitly destroy them through destructuring:

public entry fun close_vault(account: &signer) acquires Vault {
    let addr = signer::address_of(account);
    let Vault { balance: _ } = move_from<Vault>(addr);
}

Why Resources Matter

Resources solve a fundamental problem in blockchain programming: preventing the duplication or loss of digital assets.

  • No duplication: Because resources cannot be copied, a token cannot be "double-spent" at the language level.
  • No accidental loss: Because resources without drop cannot be silently discarded, assets cannot be accidentally lost.
  • Explicit lifecycle: Every resource must be explicitly created, stored, transferred, or destroyed.

This is what makes Move uniquely safe for financial applications compared to languages like Solidity, where asset safety relies on developer discipline rather than compiler enforcement.

Structs and Abilities

Abilities are the mechanism Move uses to control what operations can be performed on a type. Every struct must explicitly declare which abilities it has. This chapter explores each ability in detail.

The Four Abilities

AbilityWhat it allows
copyThe value can be duplicated
dropThe value can be implicitly destroyed when it goes out of scope
storeThe value can be stored inside another struct in global storage
keyThe value can be stored directly in global storage as a top-level resource

copy -- Duplicating Values

The copy ability allows a value to be duplicated. Without it, assigning a value to a new variable moves it, making the original invalid.

struct Point has copy, drop {
    x: u64,
    y: u64,
}

fun copy_example() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1;  // p1 is copied, both p1 and p2 are valid
    let sum = p1.x + p2.x; // Works because Point has copy
}

A struct can only have copy if all of its fields also have copy.

drop -- Implicit Destruction

The drop ability allows a value to be automatically destroyed when it goes out of scope. Without drop, every value must be explicitly consumed.

struct TempData has drop {
    value: u64,
}

fun drop_example() {
    let data = TempData { value: 42 };
    // data is implicitly dropped at end of scope -- no error
}

Without drop, the compiler would require you to destructure or move the value:

struct ImportantData {
    value: u64,
}

fun no_drop_example() {
    let data = ImportantData { value: 42 };
    let ImportantData { value: _ } = data; // Must explicitly destroy
}

store -- Nested Storage

The store ability allows a value to be stored as a field inside another struct that is in global storage. Without store, a type cannot appear inside a resource.

struct Metadata has store, copy, drop {
    name: vector<u8>,
    version: u64,
}

struct Contract has key {
    metadata: Metadata, // Metadata needs `store` to be here
    owner: address,
}

key -- Top-Level Storage

The key ability allows a struct to be stored directly in global storage under an address. This is what makes a struct a "resource" in the traditional Move sense.

struct Balance has key {
    amount: u64,
}

fun store_balance(account: &signer, amount: u64) {
    move_to(account, Balance { amount });
}

A struct with key implicitly requires all its fields to have store.

Common Ability Combinations

Pure Data (Value Type)

/// Freely copyable and droppable -- behaves like a primitive
struct Config has copy, drop, store {
    max_supply: u64,
    is_active: bool,
}

Storable Asset (Resource)

/// Stored in global storage, cannot be copied or implicitly dropped
struct Token has key, store {
    id: u64,
    value: u64,
}

Temporary Object

/// Can be dropped but not copied or stored
struct Receipt has drop {
    amount: u64,
    timestamp: u64,
}

Hot Potato (No Abilities)

A struct with no abilities at all is called a "hot potato." It must be consumed by some specific function, which is useful for enforcing certain patterns.

/// Must be consumed -- cannot be copied, dropped, or stored
struct FlashLoanReceipt {
    amount: u64,
    fee: u64,
}

Ability Constraints on Fields

A struct's abilities are constrained by its fields:

  • A struct can have copy only if all fields have copy.
  • A struct can have drop only if all fields have drop.
  • A struct can have store only if all fields have store.
  • A struct can have key only if all fields have store.
// This would NOT compile because String does not have copy:
// struct BadExample has copy, drop {
//     name: String,  // String does not have copy
// }

Best Practices

  1. Start with minimal abilities: Only add what you need.
  2. Use key for top-level resources: Structs stored under an address need key.
  3. Omit copy for assets: Digital assets should not be copyable.
  4. Omit drop for assets that must be tracked: Prevent accidental loss by omitting drop.
  5. Use hot potato pattern for receipts: When you need to ensure a value is consumed by a specific function.

Example Program Using Structs

Let's build a practical example that uses structs to create a simple on-chain address book. This will demonstrate struct definition, creation, storage, reading, updating, and destruction.

The Address Book Contract

/// A simple on-chain address book that stores contact information.
module my_addr::address_book {
    use std::signer;
    use std::string::String;
    use std::vector;

    /// No address book found for this account
    const E_NO_ADDRESS_BOOK: u64 = 1;
    /// Contact not found in the address book
    const E_CONTACT_NOT_FOUND: u64 = 2;
    /// Address book already exists
    const E_ALREADY_EXISTS: u64 = 3;

    /// A single contact entry
    struct Contact has store, drop, copy {
        name: String,
        phone: String,
    }

    /// The address book resource stored under an account
    struct AddressBook has key {
        contacts: vector<Contact>,
        owner: address,
    }

    /// Create a new address book for the caller
    public entry fun create_address_book(account: &signer) {
        let addr = signer::address_of(account);
        assert!(!exists<AddressBook>(addr), E_ALREADY_EXISTS);
        move_to(account, AddressBook {
            contacts: vector[],
            owner: addr,
        });
    }

    /// Add a contact to the address book
    public entry fun add_contact(
        account: &signer,
        name: String,
        phone: String,
    ) acquires AddressBook {
        let addr = signer::address_of(account);
        assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);

        let book = &mut AddressBook[addr];
        book.contacts.push_back(Contact { name, phone });
    }

    /// Remove a contact by index
    public entry fun remove_contact(
        account: &signer,
        index: u64,
    ) acquires AddressBook {
        let addr = signer::address_of(account);
        assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);

        let book = &mut AddressBook[addr];
        assert!(index < book.contacts.length(), E_CONTACT_NOT_FOUND);
        book.contacts.remove(index);
    }

    /// View all contacts
    #[view]
    public fun get_contacts(addr: address): vector<Contact> acquires AddressBook {
        assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);
        AddressBook[addr].contacts
    }

    /// View the number of contacts
    #[view]
    public fun contact_count(addr: address): u64 acquires AddressBook {
        assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);
        AddressBook[addr].contacts.length()
    }

    /// Remove the address book entirely
    public entry fun delete_address_book(account: &signer) acquires AddressBook {
        let addr = signer::address_of(account);
        assert!(exists<AddressBook>(addr), E_NO_ADDRESS_BOOK);
        let AddressBook { contacts: _, owner: _ } = move_from<AddressBook>(addr);
    }

    // ---- Tests ----

    #[test(account = @0x1)]
    fun test_create_and_add_contact(account: &signer) acquires AddressBook {
        let addr = signer::address_of(account);

        create_address_book(account);
        assert!(exists<AddressBook>(addr));
        assert!(contact_count(addr) == 0);

        add_contact(
            account,
            std::string::utf8(b"Alice"),
            std::string::utf8(b"555-1234"),
        );
        assert!(contact_count(addr) == 1);

        let contacts = get_contacts(addr);
        let first = contacts[0];
        assert!(first.name == std::string::utf8(b"Alice"));
    }

    #[test(account = @0x1)]
    fun test_delete_address_book(account: &signer) acquires AddressBook {
        let addr = signer::address_of(account);
        create_address_book(account);
        assert!(exists<AddressBook>(addr));

        delete_address_book(account);
        assert!(!exists<AddressBook>(addr));
    }

    #[test(account = @0x1)]
    fun test_remove_contact(account: &signer) acquires AddressBook {
        let addr = signer::address_of(account);
        create_address_book(account);
        add_contact(account, std::string::utf8(b"Alice"), std::string::utf8(b"555-1234"));
        add_contact(account, std::string::utf8(b"Bob"), std::string::utf8(b"555-5678"));
        assert!(contact_count(addr) == 2);

        remove_contact(account, 0);
        assert!(contact_count(addr) == 1);
    }

    #[test(account = @0x1)]
    #[expected_failure(abort_code = E_ALREADY_EXISTS)]
    fun test_double_create_fails(account: &signer) {
        create_address_book(account);
        create_address_book(account);
    }

    #[test(account = @0x1)]
    #[expected_failure(abort_code = E_CONTACT_NOT_FOUND)]
    fun test_remove_invalid_index_fails(account: &signer) acquires AddressBook {
        create_address_book(account);
        remove_contact(account, 0); // No contacts -- should fail
    }
}

Breakdown

Struct Design

  • Contact has store, drop, and copy -- it is a lightweight data value that can be freely duplicated and is embeddable inside other stored types.
  • AddressBook has only key -- it is a resource that lives in global storage and cannot be copied or accidentally dropped.

Entry Functions

  • create_address_book stores a new empty address book under the caller's account.
  • add_contact appends a contact to the caller's existing address book.
  • remove_contact removes a contact by index, using E_CONTACT_NOT_FOUND if the index is out of bounds.
  • delete_address_book removes and destructures the resource, freeing the storage slot.

View Functions

  • get_contacts returns a copy of all contacts.
  • contact_count returns the number of contacts.

Error Handling

Each function validates preconditions with assert! and descriptive error codes. This is a best practice for all Move contracts.

Tests

The test functions verify the full lifecycle: creation, modification, reading, and deletion. The #[expected_failure] test ensures that duplicate creation is properly rejected.

Key Takeaways

  1. Structs model your domain: Use them to represent entities and their relationships.
  2. Abilities control behavior: Choose abilities carefully to enforce safety properties.
  3. Resources live in global storage: Use move_to, move_from, and borrowing operations.
  4. Test everything: Move's built-in testing makes it easy to verify correctness.

Struct Method Syntax

Move supports defining methods on structs using a receiver-style syntax. This allows you to call functions using dot notation on struct values, making code more readable and object-oriented in style.

Defining Methods

A method is a function whose first parameter is a reference to the struct type. The method can then be called using dot notation.

module my_addr::counter {
    struct Counter has key {
        value: u64,
    }

    /// Read the current value -- called as counter.value()
    public fun value(self: &Counter): u64 {
        self.value
    }

    /// Increment the counter -- called as counter.increment()
    public fun increment(self: &mut Counter) {
        self.value = self.value + 1;
    }
}

Calling Methods

Once defined, methods can be called with dot notation:

fun example() acquires Counter {
    let counter_ref = &Counter[@0x1];
    let val = counter_ref.value();      // Method call

    let counter_mut = &mut Counter[@0x1];
    counter_mut.increment();             // Mutable method call
}

Receiver Types

Methods can take three types of receivers:

Immutable Reference (&Self)

Used for read-only operations:

public fun is_empty(self: &Counter): bool {
    self.value == 0
}

Mutable Reference (&mut Self)

Used for operations that modify the struct:

public fun reset(self: &mut Counter) {
    self.value = 0;
}

By Value (Self)

Used for operations that consume the struct:

public fun destroy(self: Counter): u64 {
    let Counter { value } = self;
    value
}

Practical Example

Here is a more complete example showing methods in a token module:

module my_addr::simple_token {
    use std::string::String;

    struct Token has store {
        name: String,
        balance: u64,
    }

    /// Create a new token
    public fun new(name: String, initial_balance: u64): Token {
        Token { name, balance: initial_balance }
    }

    /// Get the token name
    public fun name(self: &Token): &String {
        &self.name
    }

    /// Get the current balance
    public fun balance(self: &Token): u64 {
        self.balance
    }

    /// Check if the token has any balance
    public fun has_balance(self: &Token): bool {
        self.balance > 0
    }

    /// Add to the balance
    public fun deposit(self: &mut Token, amount: u64) {
        self.balance = self.balance + amount;
    }

    /// Subtract from the balance
    public fun withdraw(self: &mut Token, amount: u64): u64 {
        assert!(self.balance >= amount, 1);
        self.balance = self.balance - amount;
        amount
    }

    /// Consume the token and return the balance
    public fun redeem(self: Token): u64 {
        let Token { name: _, balance } = self;
        balance
    }
}

Usage with dot notation:

fun example() {
    let mut token = simple_token::new(
        std::string::utf8(b"MyToken"),
        1000,
    );

    let name = token.name();
    let bal = token.balance();
    assert!(token.has_balance());

    token.deposit(500);
    let withdrawn = token.withdraw(200);
    let remaining = token.redeem();
}

Method Resolution

When you write x.method(), the compiler looks for a function named method in the module where the type of x is defined. The first parameter of that function must be a reference to (or value of) the type of x.

The compiler automatically inserts the appropriate borrow. For example:

let counter = Counter { value: 0 };
counter.value()  // Compiler inserts &counter as the first argument

Best Practices

  1. Use methods for type-specific operations: Any function that logically belongs to a struct should be defined as a method.
  2. Prefer &self over &mut self: Use the least permissive receiver type.
  3. Name methods clearly: Use verbs for actions (deposit, withdraw) and nouns for accessors (balance, name).
  4. Keep methods focused: Each method should do one thing well.

Enums and Pattern Matching

Enums (enumerations) allow you to define a type by listing its possible variants. While a struct groups related fields together, an enum says "this value is one of these possible things." Combined with pattern matching via match, enums enable expressive and safe handling of multiple states.

Why Enums?

In smart contract development, you frequently encounter situations where a value can be one of several distinct types:

  • A transaction result is either a success or a failure.
  • A game character's state is idle, moving, attacking, or defeated.
  • A proposal's status is pending, approved, or rejected.

Enums make these states explicit and let the compiler verify that you handle every possible case.

What You'll Learn

  1. Defining Enums - How to declare enums with and without data
  2. The match Control Flow Construct - How to branch on enum variants

Defining Enums

Enums in Move allow you to define a type that can be one of several variants. Each variant can optionally carry data.

Basic Enum

The simplest enum is a set of named variants with no associated data:

module my_addr::status {
    enum Status has copy, drop {
        Active,
        Inactive,
        Suspended,
    }
}

Enums with Data

Variants can carry data, similar to structs:

module my_addr::shapes {
    enum Shape has copy, drop {
        Circle { radius: u64 },
        Rectangle { width: u64, height: u64 },
        Triangle { base: u64, height: u64 },
    }
}

Each variant can have different fields, making enums far more flexible than simple constants.

Enums with Positional Fields

Variants can also carry unnamed (positional) data:

module my_addr::result {
    use std::string::String;

    enum Result has copy, drop {
        Ok(u64),
        Err(String),
    }
}

Abilities on Enums

Like structs, enums can have abilities. The rules are the same: an enum can only have an ability if all data in all variants supports that ability.

/// Copyable, droppable enum
enum Color has copy, drop, store {
    Red,
    Green,
    Blue,
    Custom { r: u8, g: u8, b: u8 },
}

Creating Enum Values

You create an enum value by specifying the variant:

fun create_examples() {
    let active = Status::Active;
    let circle = Shape::Circle { radius: 10 };
    let ok = Result::Ok(42);
}

Enum Use Cases in Smart Contracts

Proposal Status

module my_addr::governance {
    enum ProposalStatus has copy, drop, store {
        Pending,
        Active { votes_for: u64, votes_against: u64 },
        Approved,
        Rejected,
        Executed,
    }

    struct Proposal has key {
        description: vector<u8>,
        status: ProposalStatus,
    }
}

Order Types

module my_addr::exchange {
    enum OrderType has copy, drop, store {
        Market,
        Limit { price: u64 },
        StopLoss { trigger_price: u64 },
    }
}

Best Practices

  1. Use enums for mutually exclusive states: If a value can only be one of several options, use an enum.
  2. Prefer enums over integer constants: Enums are type-safe and self-documenting.
  3. Attach data to variants: If a variant needs additional context, embed it directly rather than using separate fields.
  4. Add abilities thoughtfully: Only add abilities that all variants can support.

The match Control Flow Construct

The match expression lets you compare a value against a series of patterns and execute code based on which pattern matches. It is the primary way to work with enums in Move.

Basic Match

module my_addr::matcher {
    enum Coin has copy, drop {
        Penny,
        Nickel,
        Dime,
        Quarter,
    }

    fun value_in_cents(coin: Coin): u64 {
        match (coin) {
            Coin::Penny => 1,
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter => 25,
        }
    }
}

Matching with Data

When a variant carries data, you can bind the data to variables in the pattern:

enum Shape has copy, drop {
    Circle { radius: u64 },
    Rectangle { width: u64, height: u64 },
}

fun area(shape: Shape): u64 {
    match (shape) {
        Shape::Circle { radius } => {
            // Approximate: 3 * r^2
            3 * radius * radius
        },
        Shape::Rectangle { width, height } => {
            width * height
        },
    }
}

Matching Positional Variants

enum Result has copy, drop {
    Ok(u64),
    Err(u64),
}

fun unwrap_or_default(result: Result): u64 {
    match (result) {
        Result::Ok(value) => value,
        Result::Err(_) => 0,
    }
}

Exhaustiveness

The Move compiler requires that match expressions are exhaustive -- every possible variant must be handled. This prevents bugs where a case is accidentally forgotten.

// This would NOT compile -- missing Coin::Quarter:
// fun broken(coin: Coin): u64 {
//     match (coin) {
//         Coin::Penny => 1,
//         Coin::Nickel => 5,
//         Coin::Dime => 10,
//     }
// }

Multi-Line Match Arms

Match arms can contain multiple statements within braces:

fun describe(shape: Shape): vector<u8> {
    match (shape) {
        Shape::Circle { radius } => {
            if (radius > 100) {
                b"large circle"
            } else {
                b"small circle"
            }
        },
        Shape::Rectangle { width, height } => {
            if (width == height) {
                b"square"
            } else {
                b"rectangle"
            }
        },
    }
}

Match and References

You can match on references to avoid consuming the value:

fun is_circle(shape: &Shape): bool {
    match (shape) {
        Shape::Circle { .. } => true,
        _ => false,
    }
}

Practical Example: State Machine

Enums and match are ideal for implementing state machines in smart contracts:

module my_addr::auction {
    use std::signer;

    enum AuctionState has copy, drop, store {
        Open { highest_bid: u64, highest_bidder: address },
        Closed,
        Settled { winner: address, amount: u64 },
    }

    struct Auction has key {
        state: AuctionState,
        seller: address,
    }

    public fun process_bid(auction: &mut Auction, bidder: address, amount: u64) {
        auction.state = match (auction.state) {
            AuctionState::Open { highest_bid, highest_bidder } => {
                if (amount > highest_bid) {
                    AuctionState::Open {
                        highest_bid: amount,
                        highest_bidder: bidder,
                    }
                } else {
                    AuctionState::Open { highest_bid, highest_bidder }
                }
            },
            AuctionState::Closed => abort 1,  // Cannot bid on closed auction
            AuctionState::Settled { .. } => abort 2,  // Already settled
        };
    }
}

Best Practices

  1. Always handle all variants: The compiler enforces this, which prevents missing-case bugs.
  2. Use _ for unused bindings: If you don't need a value, use _ to ignore it.
  3. Keep match arms simple: If a match arm is complex, extract the logic into a separate function.
  4. Use enums for state machines: The combination of enums and match naturally models state transitions.

Managing Modules and Packages

Move code is organized into modules and packages. Understanding how to structure, import, and publish your code is essential for building maintainable smart contracts.

  • A module is the basic unit of code organization -- a named collection of types, functions, and constants published at a specific address.
  • A package is a collection of modules along with a manifest (Move.toml) that specifies metadata and dependencies.

What You'll Learn

  1. Module Basics - Defining and structuring modules
  2. Module Imports - Using code from other modules
  3. Function Scopes - Visibility levels for functions
  4. Package Basics - The Move.toml manifest and project layout
  5. Package Dependencies - Adding and managing external dependencies
  6. Package Publishing - Deploying your code on-chain

Module Basics

A module is the fundamental organizational unit in Move. It groups related types, functions, and constants together and is published at a specific blockchain address.

Defining a Module

module my_addr::greeting {
    use std::string::String;

    /// A greeting resource
    struct Greeting has key {
        message: String,
    }

    /// Store a greeting
    public entry fun set_greeting(account: &signer, msg: String) {
        move_to(account, Greeting { message: msg });
    }

    /// Read a greeting
    #[view]
    public fun get_greeting(addr: address): String acquires Greeting {
        Greeting[addr].message
    }
}

The module declaration has two parts:

  • my_addr is a named address that is resolved at compile time.
  • greeting is the module name, which must be unique within the address.

Module Contents

A module can contain:

Constants

module my_addr::config {
    /// Maximum allowed value
    const MAX_VALUE: u64 = 1000;

    /// Error: value exceeds maximum
    const E_VALUE_TOO_HIGH: u64 = 1;
}

Structs and Enums

module my_addr::types {
    struct Point has copy, drop, store {
        x: u64,
        y: u64,
    }

    enum Direction has copy, drop {
        North,
        South,
        East,
        West,
    }
}

Functions

module my_addr::math {
    public fun add(a: u64, b: u64): u64 {
        a + b
    }

    fun internal_helper(x: u64): u64 {
        x * 2
    }
}

Named Addresses

Named addresses are placeholders that are resolved at compile or deploy time. They are defined in the Move.toml file:

[addresses]
my_addr = "_"  # Will be filled at deploy time

Or set explicitly:

[addresses]
my_addr = "0x1234abcd"

When deploying with the Aptos CLI, you provide the address mapping:

aptos move publish --named-addresses my_addr=default

Module Naming Conventions

  • Module names use snake_case: my_module, token_factory.
  • Struct and enum names use PascalCase: UserProfile, OrderType.
  • Function names use snake_case: create_account, get_balance.
  • Constants use UPPER_SNAKE_CASE: MAX_SUPPLY, E_NOT_FOUND.
  • Error constants begin with E_ or E: E_INSUFFICIENT_BALANCE.

One Module Per Purpose

A best practice is to keep each module focused on a single responsibility:

sources/
├── token.move        # Token struct and logic
├── marketplace.move  # Marketplace for trading tokens
├── governance.move   # Voting and proposals
└── config.move       # Shared configuration

This makes your code easier to read, test, and upgrade.

Module Imports

Move uses the use keyword to import modules, types, and functions from other modules. This lets you reference external code without writing out full paths every time.

Basic Imports

Importing a Module

module my_addr::example {
    use std::string;

    fun create_greeting(): string::String {
        string::utf8(b"Hello, Aptos!")
    }
}

Importing a Specific Type

module my_addr::example {
    use std::string::String;

    fun create_greeting(): String {
        std::string::utf8(b"Hello, Aptos!")
    }
}

Importing Both Module and Type

Use Self to import the module itself alongside specific types:

module my_addr::example {
    use std::string::{Self, String};

    fun create_greeting(): String {
        string::utf8(b"Hello, Aptos!")
    }
}

This is the most common pattern -- it gives you access to both the String type directly and the module's functions via string::.

Multiple Imports

From the Same Module

use std::vector::{Self, empty, push_back, length};

From Different Modules

use std::string::{Self, String};
use std::signer;
use std::vector;
use aptos_framework::event;
use aptos_framework::account;

Standard Library Addresses

Aptos has three standard library addresses:

AddressNameDescription
std (0x1)Move Standard LibraryBasic types, vectors, strings, options
aptos_std (0x1)Aptos Standard LibraryExtended utilities, crypto, data structures
aptos_framework (0x1)Aptos FrameworkAccounts, coins, objects, governance

Common imports:

// From Move Stdlib
use std::string::{Self, String};
use std::vector;
use std::option::{Self, Option};
use std::signer;
use std::error;

// From Aptos Stdlib
use aptos_std::table::{Self, Table};
use aptos_std::smart_table::{Self, SmartTable};

// From Aptos Framework
use aptos_framework::event;
use aptos_framework::object::{Self, Object};
use aptos_framework::fungible_asset::{Self, FungibleAsset};
use aptos_framework::primary_fungible_store;

Aliasing Imports

You can rename imports with as:

use std::string::String as Str;
use aptos_framework::fungible_asset as fa;

Where to Place Imports

Imports are placed at the top of the module body, before any definitions:

module my_addr::example {
    // Imports first
    use std::string::{Self, String};
    use std::signer;
    use aptos_framework::event;

    // Then constants, structs, and functions
    const E_NOT_FOUND: u64 = 1;

    struct MyStruct has key {
        name: String,
    }

    public fun my_function() { }
}

Test-Only Imports

Use #[test_only] to import modules that are only needed in tests:

#[test_only]
use std::debug::print;

#[test_only]
use aptos_framework::account::create_account_for_test;

Best Practices

  1. Import what you use: Don't import modules you don't reference.
  2. Use Self pattern: Import both the module and its key types with use module::{Self, Type}.
  3. Group imports logically: Group std, aptos_std, aptos_framework, and your own modules.
  4. Prefer specific imports: Import specific types rather than using full paths throughout.

Function Scopes

Move provides several visibility levels for functions that control where they can be called from. Choosing the right scope is important for security and API design.

Visibility Levels

Private (default)

Functions with no visibility modifier are private. They can only be called from within the same module.

module my_addr::example {
    /// Only callable within this module
    fun internal_calculation(x: u64): u64 {
        x * 2
    }

    public fun public_function(): u64 {
        internal_calculation(21) // OK -- same module
    }
}

Public

public functions can be called from any module.

module my_addr::math {
    /// Callable from anywhere
    public fun add(a: u64, b: u64): u64 {
        a + b
    }
}

Public Entry

entry functions can be called as standalone transactions from outside the blockchain (e.g., from a wallet or SDK). They can be combined with public to also be callable from other modules.

module my_addr::token {
    /// Callable as a transaction AND from other modules
    public entry fun transfer(from: &signer, to: address, amount: u64) {
        // ...
    }

    /// Callable as a transaction ONLY (not from other modules)
    entry fun admin_action(admin: &signer) {
        // ...
    }
}

Public(friend)

public(friend) functions are callable only from modules that are declared as friends.

module my_addr::core {
    friend my_addr::helper;

    /// Only callable from my_addr::helper
    public(friend) fun internal_mint(amount: u64) {
        // ...
    }
}

module my_addr::helper {
    use my_addr::core;

    public fun do_mint(amount: u64) {
        core::internal_mint(amount); // OK -- declared as friend
    }
}

Public(package)

public(package) functions are callable from any module within the same package but not from external packages.

module my_addr::internal_api {
    /// Callable from any module in this package
    public(package) fun package_only_function(): u64 {
        42
    }
}

View Functions

The #[view] attribute marks a function as callable by external read queries without submitting a transaction. View functions should not modify state.

#[view]
public fun get_balance(addr: address): u64 acquires Balance {
    Balance[addr].amount
}

Summary Table

ModifierSame ModuleSame PackageFriend ModuleAny ModuleTransaction
(private)YesNoNoNoNo
publicYesYesYesYesNo
entryYesNoNoNoYes
public entryYesYesYesYesYes
public(friend)YesNoYesNoNo
public(package)YesYesNoNoNo

Best Practices

  1. Start private: Make functions private by default and only increase visibility as needed.
  2. Use entry for user-facing actions: Any function a user calls directly should be entry.
  3. Use public for reusable library functions: Functions other modules need to call.
  4. Use public(package) for internal APIs: Shared logic within your package that shouldn't be exposed externally.
  5. Use #[view] for read operations: Mark all pure read functions with #[view].
  6. Limit friend usage: Use sparingly -- public(package) is usually a better choice.

Package Basics

A Move package is a directory containing a Move.toml manifest and one or more Move source files. It is the unit of compilation and deployment on Aptos.

Package Structure

A typical package looks like this:

my_project/
├── Move.toml          # Package manifest
├── sources/           # Move source files
│   ├── token.move
│   └── marketplace.move
└── tests/             # Test files (optional)
    └── token_tests.move

The Move.toml File

The Move.toml manifest describes your package:

[package]
name = "MyProject"
version = "1.0.0"
authors = ["developer@example.com"]

[addresses]
my_addr = "_"

[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-framework.git", subdir = "aptos-move/framework/aptos-framework", rev = "main" }

Sections

  • [package] -- Metadata about your package (name, version, authors).
  • [addresses] -- Named address definitions. Use "_" for addresses that will be provided at deploy time.
  • [dependencies] -- External packages your code depends on.

Creating a New Package

The Aptos CLI can create a package from a template:

mkdir my_project
cd my_project
aptos move init --name MyProject

This creates the basic directory structure and a Move.toml file.

You can also use templates:

aptos move init --name MyProject --template hello-blockchain

Building a Package

To compile your package:

aptos move compile --named-addresses my_addr=default

The --named-addresses flag maps named addresses to actual values. The special value default uses your CLI profile's address.

Source Files

Move source files have the .move extension and can contain one or more modules:

// sources/token.move
module my_addr::token {
    struct Token has key {
        balance: u64,
    }
}

Multiple modules can exist in a single file, but the convention is one module per file, with the filename matching the module name.

Test Files

Test files live in the tests/ directory or alongside source files. Test functions are annotated with #[test]:

// tests/token_tests.move
#[test_only]
module my_addr::token_tests {
    use my_addr::token;

    #[test]
    fun test_creation() {
        // ...
    }
}

Best Practices

  1. One module per file: Keep source files focused and name them after the module they contain.
  2. Use named addresses: Use named addresses with "_" rather than hardcoding addresses.
  3. Version your packages: Use semantic versioning in Move.toml.
  4. Keep Move.toml clean: Only include dependencies you actually use.

Package Dependencies

Move packages can depend on other packages, including the Aptos framework, standard libraries, and third-party packages.

Adding Dependencies

Dependencies are declared in the [dependencies] section of Move.toml.

Git Dependencies

The most common type of dependency for Aptos projects:

[dependencies]
AptosFramework = {
    git = "https://github.com/aptos-labs/aptos-framework.git",
    subdir = "aptos-move/framework/aptos-framework",
    rev = "main"
}
  • git -- The repository URL.
  • subdir -- The subdirectory within the repo that contains the Move package.
  • rev -- The git revision (branch, tag, or commit hash). Using a specific commit hash is recommended for reproducible builds.

Local Dependencies

For packages on your local filesystem:

[dependencies]
MyLibrary = { local = "../my-library" }

Pinning a Specific Version

For reproducible builds, pin to a specific commit hash:

[dependencies]
AptosFramework = {
    git = "https://github.com/aptos-labs/aptos-framework.git",
    subdir = "aptos-move/framework/aptos-framework",
    rev = "abc123def456"
}

The Aptos Framework Dependencies

Most Aptos projects need the Aptos Framework, which provides standard modules for accounts, coins, objects, and more. The framework includes three layers:

[dependencies]
# Includes MoveStdlib, AptosStdlib, and AptosFramework
AptosFramework = {
    git = "https://github.com/aptos-labs/aptos-framework.git",
    subdir = "aptos-move/framework/aptos-framework",
    rev = "main"
}

Importing AptosFramework automatically includes MoveStdlib and AptosStdlib as transitive dependencies.

Address Mapping for Dependencies

If a dependency uses named addresses, you may need to provide them. The Aptos Framework standard addresses are typically set automatically.

[addresses]
my_addr = "_"
std = "0x1"
aptos_std = "0x1"
aptos_framework = "0x1"

Dependency Resolution

When you run aptos move compile, the CLI:

  1. Downloads all git dependencies to a local cache.
  2. Resolves transitive dependencies.
  3. Checks for version conflicts.
  4. Compiles all dependencies before your package.

Best Practices

  1. Pin dependency versions: Use specific commit hashes rather than main for production code.
  2. Minimize dependencies: Only include what you need.
  3. Use the framework: The Aptos Framework provides battle-tested implementations of common patterns.
  4. Test with the same dependencies: Ensure your tests use the same dependency versions as your production code.

Package Publishing

Publishing a package deploys your compiled Move modules to the Aptos blockchain. Once published, your code is live and callable by anyone.

Publishing with the Aptos CLI

The most common way to publish is with the aptos move publish command (or aptos move deploy):

aptos move publish --named-addresses my_addr=default

This will:

  1. Compile your package.
  2. Show you a summary of the modules being deployed and estimated gas cost.
  3. Prompt for confirmation.
  4. Submit the publish transaction.

Specifying a Profile

If you have multiple CLI profiles, specify which to use:

aptos move publish --profile mainnet --named-addresses my_addr=mainnet

Skipping Confirmation

For scripts or CI/CD:

aptos move publish --named-addresses my_addr=default --assume-yes

Upgrade Policies

When you publish code for the first time, you can choose an upgrade policy:

  • compatible (default): You can upgrade the package, but changes must be backward-compatible (no removing public functions, no changing function signatures).
  • immutable: The code can never be changed after publication.

For most projects, compatible is the right choice. It allows you to fix bugs and add new functionality while preserving existing interfaces.

Upgrading a Package

To upgrade an already-published package, simply run the publish command again:

aptos move publish --named-addresses my_addr=default

The CLI will detect the existing package and perform an upgrade. The upgrade will fail if the changes are not backward-compatible (e.g., removing a public function).

What Can Be Changed

  • Adding new modules
  • Adding new functions (public, entry, etc.)
  • Adding new structs
  • Changing private function implementations
  • Adding new fields to structs (in some cases)

What Cannot Be Changed

  • Removing or renaming public functions
  • Changing the signature of existing public functions
  • Removing or renaming existing structs
  • Removing or renaming existing modules

Publishing Workflow

A typical workflow for deploying to mainnet:

  1. Develop and test locally:

    aptos move test --dev
    
  2. Deploy to devnet:

    aptos init --profile devnet --network devnet
    aptos move publish --profile devnet --named-addresses my_addr=devnet
    
  3. Test on devnet: Interact with your contract and verify behavior.

  4. Deploy to testnet:

    aptos init --profile testnet --network testnet
    aptos move publish --profile testnet --named-addresses my_addr=testnet
    
  5. Deploy to mainnet:

    aptos init --profile mainnet --network mainnet
    aptos move publish --profile mainnet --named-addresses my_addr=mainnet
    

Verifying Published Code

After publishing, you can verify your code on the Aptos Explorer. The source code is uploaded by default during publication, making it easy for others to verify what your contract does.

Best Practices

  1. Always test before publishing: Run aptos move test before every deployment.
  2. Deploy to devnet first: Catch issues before they reach mainnet.
  3. Use compatible upgrades: Allow yourself to fix bugs after deployment.
  4. Document your API: Once published, your public functions are your contract's interface.
  5. Keep private functions private: Only expose what external callers need.

Collections

There are two major categories of collections on Aptos:

  • Key Value Collections -> Maps and Tables
  • Ordered Collections -> Vectors

We'll go through each of these in the following pages.

Vectors

Vectors are ordered sequences of elements in Move. They are one of the most fundamental collection types and are used extensively throughout Move programs.

Available Vector Types

Standard Vector (std::vector)

The basic vector type from the Move standard library.

use std::vector;

// Create an empty vector
let empty_vec: vector<u64> = vector::empty<u64>();

// Create a vector with initial elements
let numbers: vector<u64> = vector[1, 2, 3, 4, 5];

// Create a vector of strings
let strings: vector<String> = vector[
    string::utf8(b"hello"),
    string::utf8(b"world")
];

BigVector (aptos_std::big_vector)

A specialized vector implementation for large datasets that uses table storage internally.

use aptos_std::big_vector::{Self, BigVector};

// Create a BigVector
let big_vec = big_vector::new<u64>();

// Add elements
big_vector::push_back(&mut big_vec, 1);
big_vector::push_back(&mut big_vec, 2);

How to Use Vectors

Basic Operations

module my_module::vector_example {
    use std::vector;
    use std::string::{Self, String};

    public fun create_and_manipulate_vector(): vector<u64> {
        // Create an empty vector
        let v = vector::empty<u64>();
        
        // Add elements
        vector::push_back(&mut v, 1);
        vector::push_back(&mut v, 2);
        vector::push_back(&mut v, 3);
        
        // Get length
        let len = vector::length(&v);
        
        // Access element by index
        let first = *vector::borrow(&v, 0);
        let last = *vector::borrow(&v, len - 1);
        
        // Remove and return last element
        let popped = vector::pop_back(&mut v);
        
        v
    }

    public fun vector_operations(): vector<String> {
        let v = vector::empty<String>();
        
        // Add strings
        vector::push_back(&mut v, string::utf8(b"hello"));
        vector::push_back(&mut v, string::utf8(b"world"));
        
        // Check if empty
        let is_empty = vector::is_empty(&v);
        
        // Get capacity (if available)
        let capacity = vector::length(&v);
        
        v
    }
}

Iterating Over Vectors

public fun sum_vector(v: &vector<u64>): u64 {
    let sum = 0u64;
    let i = 0;
    let len = vector::length(v);
    
    while (i < len) {
        sum = sum + *vector::borrow(v, i);
        i = i + 1;
    };
    
    sum
}

// Using for loop (Move 2024+)
public fun sum_vector_for(v: &vector<u64>): u64 {
    let sum = 0u64;
    for (element in v) {
        sum = sum + *element;
    };
    sum
}

Vector Manipulation

public fun vector_manipulation(): vector<u64> {
    let v = vector[1, 2, 3, 4, 5];
    
    // Insert at specific index
    vector::insert(&mut v, 2, 10);
    // Result: [1, 2, 10, 3, 4, 5]
    
    // Remove element at index
    let removed = vector::remove(&mut v, 1);
    // Result: [1, 10, 3, 4, 5], removed = 2
    
    // Swap elements
    vector::swap(&mut v, 0, 2);
    // Result: [3, 10, 1, 4, 5]
    
    // Reverse the vector
    vector::reverse(&mut v);
    // Result: [5, 4, 1, 10, 3]
    
    v
}

Vector Abilities and Constraints

Vectors have specific abilities that determine how they can be used:

// Vector has copy, drop, and store abilities
struct VectorHolder has key, store {
    data: vector<u64>
}

// Vectors can be stored in global storage
public fun store_vector(account: &signer) {
    let v = vector[1, 2, 3];
    move_to(account, VectorHolder { data: v });
}

// Vectors can be copied and dropped
public fun vector_abilities() {
    let v1 = vector[1, 2, 3];
    let v2 = v1; // Copy
    // v1 is still available due to copy ability
    
    // Both v1 and v2 are automatically dropped at end of function
}

Tradeoffs

Advantages of Standard Vectors

  • Simple and familiar - Easy to understand and use
  • Efficient for small datasets - Good performance for collections with < 1000 elements
  • Flexible - Supports all basic operations (push, pop, insert, remove, etc.)
  • Memory efficient - Compact storage for small collections
  • BCS serializable - Can be easily serialized and deserialized

Disadvantages of Standard Vectors

  • Limited scalability - Performance degrades with large datasets
  • Single storage slot - All elements stored in one storage slot
  • No parallelization - Operations cannot be parallelized
  • Memory constraints - Limited by single storage slot size
  • Expensive operations - Insert/remove operations are O(n)

When to Use BigVector

  • Large datasets - When you need to store thousands of elements
  • Frequent additions - When you frequently add elements
  • Table-based storage - When you want table storage benefits
  • Scalability requirements - When you need to scale to large collections

When to Use Standard Vector

  • Small datasets - Collections with < 1000 elements
  • Simple use cases - When you need basic sequential storage
  • Memory efficiency - When storage space is a concern
  • BCS serialization - When you need to serialize the data

Performance Characteristics

OperationStandard VectorBigVector
Push backO(1) amortizedO(1)
Pop backO(1)O(1)
Access by indexO(1)O(1)
Insert at indexO(n)O(n)
Remove at indexO(n)O(n)
Storage costSingle slotMultiple slots
ParallelizationNoYes (for table operations)

Best Practices

  1. Choose the right type: Use standard vectors for small collections, BigVector for large ones
  2. Prefer push_back/pop_back: These are the most efficient operations
  3. Avoid frequent insert/remove: These operations are expensive
  4. Use references when possible: Borrow elements instead of copying when you only need to read
  5. Consider storage costs: Vectors stored in global storage consume storage slots
  6. Plan for growth: If your collection might grow large, consider BigVector from the start

Maps

Maps are key-value storage structures that allow efficient lookup and storage of data by unique keys. Aptos provides several map implementations with different characteristics and use cases.

Available Map Types

SimpleMap (std::simple_map)

A basic hash map implementation from the Move standard library.

use std::simple_map::{Self, SimpleMap};

// Create an empty map
let map: SimpleMap<address, u64> = simple_map::create<address, u64>();

// Add key-value pairs
simple_map::add(&mut map, @0x1, 100);
simple_map::add(&mut map, @0x2, 200);

OrderedMap (std::ordered_map)

A map implementation that maintains insertion order of keys.

use std::ordered_map::{Self, OrderedMap};

// Create an ordered map
let ordered_map: OrderedMap<u64, String> = ordered_map::new<u64, String>();

// Add elements (maintains order)
ordered_map::add(&mut ordered_map, 1, string::utf8(b"first"));
ordered_map::add(&mut ordered_map, 2, string::utf8(b"second"));

BigOrderedMap (aptos_std::big_ordered_map)

A scalable ordered map implementation for large datasets using table storage.

use aptos_std::big_ordered_map::{Self, BigOrderedMap};

// Create a big ordered map
let big_map = big_ordered_map::new<address, u64>();

How to Use Maps

Basic Operations with SimpleMap

module my_module::map_example {
    use std::simple_map::{Self, SimpleMap};
    use std::string::{Self, String};

    public fun create_and_use_map(): SimpleMap<address, u64> {
        // Create an empty map
        let map = simple_map::create<address, u64>();
        
        // Add key-value pairs
        simple_map::add(&mut map, @0x1, 100);
        simple_map::add(&mut map, @0x2, 200);
        simple_map::add(&mut map, @0x3, 300);
        
        // Check if key exists
        let has_key = simple_map::contains_key(&map, &@0x1);
        
        // Get value by key
        let value = *simple_map::borrow(&map, &@0x2);
        
        // Update existing value
        simple_map::set(&mut map, @0x1, 150);
        
        // Remove key-value pair
        let removed_value = simple_map::remove(&mut map, &@0x3);
        
        map
    }

    public fun map_with_strings(): SimpleMap<String, u64> {
        let map = simple_map::create<String, u64>();
        
        simple_map::add(&mut map, string::utf8(b"alice"), 1000);
        simple_map::add(&mut map, string::utf8(b"bob"), 2000);
        simple_map::add(&mut map, string::utf8(b"charlie"), 3000);
        
        map
    }
}

Working with OrderedMap

public fun ordered_map_operations(): OrderedMap<u64, String> {
    let map = ordered_map::new<u64, String>();
    
    // Add elements (order is preserved)
    ordered_map::add(&mut map, 3, string::utf8(b"third"));
    ordered_map::add(&mut map, 1, string::utf8(b"first"));
    ordered_map::add(&mut map, 2, string::utf8(b"second"));
    
    // Get length
    let len = ordered_map::length(&map);
    
    // Check if empty
    let is_empty = ordered_map::is_empty(&map);
    
    // Get value by key
    let value = *ordered_map::borrow(&map, &1);
    
    // Update value
    ordered_map::set(&mut map, 1, string::utf8(b"updated_first"));
    
    // Remove key-value pair
    let removed = ordered_map::remove(&mut map, &3);
    
    map
}

Iterating Over Maps

public fun iterate_simple_map(map: &SimpleMap<address, u64>): u64 {
    let total = 0u64;
    let keys = simple_map::keys(map);
    let i = 0;
    let len = vector::length(&keys);
    
    while (i < len) {
        let key = *vector::borrow(&keys, i);
        let value = *simple_map::borrow(map, &key);
        total = total + value;
        i = i + 1;
    };
    
    total
}

public fun iterate_ordered_map(map: &OrderedMap<u64, String>): vector<String> {
    let values = vector::empty<String>();
    let keys = ordered_map::keys(map);
    let i = 0;
    let len = vector::length(&keys);
    
    while (i < len) {
        let key = *vector::borrow(&keys, i);
        let value = *ordered_map::borrow(map, &key);
        vector::push_back(&mut values, value);
        i = i + 1;
    };
    
    values
}

Map Abilities and Storage

// Maps can be stored in global storage
struct MapHolder has key, store {
    user_balances: SimpleMap<address, u64>,
    user_names: OrderedMap<address, String>
}

public fun store_maps(account: &signer) {
    let balances = simple_map::create<address, u64>();
    let names = ordered_map::new<address, String>();
    
    // Add some data
    simple_map::add(&mut balances, @0x1, 1000);
    ordered_map::add(&mut names, @0x1, string::utf8(b"Alice"));
    
    move_to(account, MapHolder {
        user_balances: balances,
        user_names: names
    });
}

Tradeoffs

SimpleMap Advantages

  • Simple and efficient - Basic hash map implementation
  • Fast lookups - O(1) average case for key lookups
  • Memory efficient - Compact storage for small to medium datasets
  • Standard library - Part of Move standard library
  • BCS serializable - Can be easily serialized

SimpleMap Disadvantages

  • No ordering - Keys are not stored in any particular order
  • Limited scalability - Performance degrades with very large datasets
  • Single storage slot - All data stored in one storage slot
  • No parallelization - Operations cannot be parallelized

OrderedMap Advantages

  • Maintains order - Keys are stored in insertion order
  • Predictable iteration - Consistent order when iterating
  • Useful for UI - Good for displaying data in a consistent order
  • Standard library - Part of Move standard library

OrderedMap Disadvantages

  • Higher overhead - Additional storage for maintaining order
  • Slower operations - Slightly slower than SimpleMap due to ordering overhead
  • Limited scalability - Similar limitations to SimpleMap

BigOrderedMap Advantages

  • Scalable - Designed for large datasets
  • Table-based storage - Uses efficient table storage
  • Parallelizable - Can benefit from table parallelization
  • Maintains order - Preserves insertion order like OrderedMap

BigOrderedMap Disadvantages

  • Higher storage cost - Uses multiple storage slots
  • More complex - More complex implementation
  • Not in standard library - Part of Aptos standard library

Performance Characteristics

OperationSimpleMapOrderedMapBigOrderedMap
InsertO(1) avgO(1) avgO(1) avg
LookupO(1) avgO(1) avgO(1) avg
DeleteO(1) avgO(1) avgO(1) avg
IterationUnorderedOrderedOrdered
StorageSingle slotSingle slotMultiple slots
ParallelizationNoNoYes

When to Use Each Type

Use SimpleMap when:

  • You need basic key-value storage
  • Order doesn't matter
  • Dataset is small to medium size (< 10,000 entries)
  • You want maximum performance
  • You're working with standard library only

Use OrderedMap when:

  • You need to maintain insertion order
  • You frequently iterate over the map
  • Dataset is small to medium size
  • Order is important for your use case
  • You want predictable iteration order

Use BigOrderedMap when:

  • You have large datasets (> 10,000 entries)
  • You need both ordering and scalability
  • You want table storage benefits
  • You need parallelizable operations
  • You're building a high-scale application

Best Practices

  1. Choose the right map type: Consider your data size and ordering requirements
  2. Use appropriate key types: Prefer simple types like address or u64 for keys
  3. Handle missing keys: Always check if a key exists before accessing it
  4. Consider storage costs: Maps stored in global storage consume storage slots
  5. Plan for growth: If your map might grow large, consider BigOrderedMap from the start
  6. Use references: Borrow values instead of copying when you only need to read
  7. Batch operations: When possible, batch multiple operations together

Tables

Tables are hash-addressable storage structures designed for large-scale data storage on Aptos. They provide efficient storage and retrieval of data using unique keys, with each table entry stored in its own storage slot for better performance and parallelization.

Available Table Types

Table (aptos_std::table)

The basic table implementation that provides hash-addressable storage.

use aptos_std::table::{Self, Table};

// Create a new table
let table: Table<address, u64> = table::new<address, u64>();

// Add key-value pairs
table::add(&mut table, @0x1, 100);
table::add(&mut table, @0x2, 200);

TableWithLength (aptos_std::table_with_length)

A table implementation that tracks the number of entries and allows full deletion.

use aptos_std::table_with_length::{Self, TableWithLength};

// Create a table with length tracking
let table: TableWithLength<address, u64> = table_with_length::new<address, u64>();

// Add elements (length is automatically tracked)
table_with_length::add(&mut table, @0x1, 100);

SmartTable (aptos_std::smart_table)

A bucketed table implementation that groups entries into vectors for more efficient storage.

use aptos_std::smart_table::{Self, SmartTable};

// Create a smart table
let smart_table: SmartTable<address, u64> = smart_table::new<address, u64>();

// Add elements (automatically bucketed)
smart_table::add(&mut smart_table, @0x1, 100);

How to Use Tables

Basic Operations with Table

module my_module::table_example {
    use aptos_std::table::{Self, Table};
    use std::string::{Self, String};

    public fun create_and_use_table(): Table<address, u64> {
        // Create a new table
        let table = table::new<address, u64>();
        
        // Add key-value pairs
        table::add(&mut table, @0x1, 100);
        table::add(&mut table, @0x2, 200);
        table::add(&mut table, @0x3, 300);
        
        // Check if key exists
        let has_key = table::contains(&table, @0x1);
        
        // Get value by key
        let value = *table::borrow(&table, @0x2);
        
        // Update existing value
        table::set(&mut table, @0x1, 150);
        
        // Remove key-value pair
        let removed_value = table::remove(&mut table, @0x3);
        
        table
    }

    public fun table_with_strings(): Table<String, u64> {
        let table = table::new<String, u64>();
        
        table::add(&mut table, string::utf8(b"alice"), 1000);
        table::add(&mut table, string::utf8(b"bob"), 2000);
        table::add(&mut table, string::utf8(b"charlie"), 3000);
        
        table
    }
}

Working with TableWithLength

public fun table_with_length_operations(): TableWithLength<u64, String> {
    let table = table_with_length::new<u64, String>();
    
    // Add elements (length is tracked automatically)
    table_with_length::add(&mut table, 1, string::utf8(b"first"));
    table_with_length::add(&mut table, 2, string::utf8(b"second"));
    table_with_length::add(&mut table, 3, string::utf8(b"third"));
    
    // Get length
    let len = table_with_length::length(&table);
    
    // Check if empty
    let is_empty = table_with_length::is_empty(&table);
    
    // Get value by key
    let value = *table_with_length::borrow(&table, 1);
    
    // Update value
    table_with_length::set(&mut table, 1, string::utf8(b"updated_first"));
    
    // Remove key-value pair
    let removed = table_with_length::remove(&mut table, 3);
    
    // Length is automatically updated
    let new_len = table_with_length::length(&table);
    
    table
}

Using SmartTable

public fun smart_table_operations(): SmartTable<address, u64> {
    let table = smart_table::new<address, u64>();
    
    // Add elements (automatically bucketed)
    smart_table::add(&mut table, @0x1, 100);
    smart_table::add(&mut table, @0x2, 200);
    smart_table::add(&mut table, @0x3, 300);
    
    // Get length
    let len = smart_table::length(&table);
    
    // Check if empty
    let is_empty = smart_table::is_empty(&table);
    
    // Get value by key
    let value = *smart_table::borrow(&table, @0x2);
    
    // Update value
    smart_table::set(&mut table, @0x1, 150);
    
    // Remove key-value pair
    let removed = smart_table::remove(&mut table, @0x3);
    
    table
}

Table Abilities and Storage

// Tables can be stored in global storage
struct TableHolder has key, store {
    user_balances: Table<address, u64>,
    user_profiles: TableWithLength<address, String>,
    user_scores: SmartTable<address, u64>
}

public fun store_tables(account: &signer) {
    let balances = table::new<address, u64>();
    let profiles = table_with_length::new<address, String>();
    let scores = smart_table::new<address, u64>();
    
    // Add some data
    table::add(&mut balances, @0x1, 1000);
    table_with_length::add(&mut profiles, @0x1, string::utf8(b"Alice"));
    smart_table::add(&mut scores, @0x1, 95);
    
    move_to(account, TableHolder {
        user_balances: balances,
        user_profiles: profiles,
        user_scores: scores
    });
}

Iterating Over Tables

public fun iterate_table(table: &Table<address, u64>): u64 {
    let total = 0u64;
    let keys = table::keys(table);
    let i = 0;
    let len = vector::length(&keys);
    
    while (i < len) {
        let key = *vector::borrow(&keys, i);
        let value = *table::borrow(table, key);
        total = total + value;
        i = i + 1;
    };
    
    total
}

// SmartTable supports direct iteration
public fun iterate_smart_table(table: &SmartTable<address, u64>): u64 {
    let total = 0u64;
    let keys = smart_table::keys(table);
    let i = 0;
    let len = vector::length(&keys);
    
    while (i < len) {
        let key = *vector::borrow(&keys, i);
        let value = *smart_table::borrow(table, key);
        total = total + value;
        i = i + 1;
    };
    
    total
}

Tradeoffs

Table Advantages

  • Hash-addressable storage - Each entry stored in its own storage slot
  • Parallelizable operations - Different entries can be processed in parallel
  • Scalable - Designed for large datasets
  • Efficient lookups - O(1) average case for key lookups
  • Individual storage slots - Each entry has its own storage slot

Table Disadvantages

  • Higher storage cost - Each entry uses a separate storage slot
  • Table handle cost - The table handle itself uses a storage slot
  • No length tracking - Basic table doesn't track number of entries
  • Handle cannot be deleted - Table handle persists even when empty

TableWithLength Advantages

  • Length tracking - Automatically tracks number of entries
  • Full deletion - Can delete the entire table including handle
  • All table benefits - Inherits all advantages of basic table
  • Useful for queries - Easy to check if table is empty or get size

TableWithLength Disadvantages

  • Not parallelizable - Length updates prevent parallelization
  • Higher overhead - Additional storage for length tracking
  • Sequential operations - Length updates must be sequential

SmartTable Advantages

  • Bucketed storage - Groups entries into vectors for efficiency
  • Length tracking - Automatically tracks number of entries
  • Iterable - Supports iteration over all entries
  • Storage efficient - Reduces number of storage slots used
  • Parallelizable - Can be parallelized for certain operations

SmartTable Disadvantages

  • DDoS vulnerability - Malicious keys could create large buckets
  • Complex implementation - More complex than basic table
  • Variable performance - Performance depends on key distribution

Performance Characteristics

OperationTableTableWithLengthSmartTable
InsertO(1) avgO(1) avgO(1) avg
LookupO(1) avgO(1) avgO(1) avg
DeleteO(1) avgO(1) avgO(1) avg
LengthN/AO(1)O(1)
IterationYesYesYes
Storage slotsN+1N+2Variable
ParallelizationYesNoYes

When to Use Each Type

Use Table when:

  • You need basic hash-addressable storage
  • You have large datasets
  • You want parallelizable operations
  • You don't need length tracking
  • You want maximum flexibility

Use TableWithLength when:

  • You need to track the number of entries
  • You want to be able to delete the entire table
  • You need to check if the table is empty
  • You don't need parallelization
  • You want all table benefits plus length tracking

Use SmartTable when:

  • You want more storage-efficient bucketing
  • You need both length tracking and iteration
  • You want to reduce storage slot usage
  • You can control key distribution
  • You need scalable storage with iteration

Best Practices

  1. Choose the right table type: Consider your specific needs for length tracking and iteration
  2. Use appropriate key types: Prefer simple types like address or u64 for keys
  3. Handle missing keys: Always check if a key exists before accessing it
  4. Consider storage costs: Each table entry uses a storage slot
  5. Plan for parallelization: Use basic Table when you need parallel operations
  6. Monitor key distribution: For SmartTable, ensure keys are well-distributed
  7. Use references: Borrow values instead of copying when you only need to read
  8. Batch operations: When possible, batch multiple operations together
  9. Consider deletion: Use TableWithLength if you need to delete the entire table
  10. Monitor storage usage: Tables can be expensive for very large datasets

Collection Types Comparison

This document provides a comprehensive comparison of all collection types available in Move on Aptos, helping you choose the right collection for your specific use case.

Quick Reference Table

Collection TypeLibraryOrderingLength TrackingScalabilityParallelizationStorage CostBest For
VectorstdYesYesLowNoSingle slotSmall sequences
BigVectoraptos_stdYesYesHighYesMultiple slotsLarge sequences
SimpleMapstdNoNoLowNoSingle slotSmall key-value
OrderedMapaptos_stdYesYesLowNoSingle slotOrdered key-value
BigOrderedMapaptos_stdYesYesHighYesMultiple slotsLarge ordered key-value
Tableaptos_stdNoNoHighYesN+1 slotsLarge conflicting key-value data sets
TableWithLengthaptos_stdNoYesHighNoN+2 slotsLarge data sets with lots of reads and low writes
SmartTableaptos_stdNoYesHighYesVariableLarge key-value (not-recommended)

Detailed Comparison

Vectors

Use Cases:

  • Storing ordered sequences of data
  • Building lists, arrays, or queues
  • When you need to maintain insertion order
  • Small to medium datasets (< 1,000 elements)

When to Choose:

  • Standard Vector: Small datasets, simple use cases, memory efficiency
  • BigVector: Large datasets, frequent additions, scalability requirements

Example:

// Standard Vector for small dataset
let small_list: vector<u64> = vector[1, 2, 3, 4, 5];

// BigVector for large dataset
let large_list = big_vector::new<u64>();
big_vector::push_back(&mut large_list, 1);

Maps

Use Cases:

  • Key-value storage and lookup
  • Associative data structures
  • When you need fast access by unique keys
  • Small to medium datasets (< 10,000 entries)

When to Choose:

  • SimpleMap: Small key-value storage, maximum space efficiency, order doesn't matter
  • OrderedMap: Need order of items, and provides O(log(n)) lookup

Example:

// SimpleMap for basic storage
let balances = simple_map::create<address, u64>();
simple_map::add(&mut balances, @0x1, 1000);

// OrderedMap for ordered storage
let profiles = ordered_map::new<address, String>();
ordered_map::add(&mut profiles, @0x1, string::utf8(b"Alice"));

Tables

Use Cases:

  • Large-scale key-value storage
  • When you need parallelizable operations
  • Hash-addressable storage requirements
  • Large datasets (> 10,000 entries)

When to Choose:

  • Table: Basic hash-addressable storage, parallelization needed
  • TableWithLength: Need length tracking, want to delete entire table, not a lot of writes (breaks parallelism)
  • BigOrderedMap: Large datasets, need both ordering and scalability
  • SmartTable: Storage efficiency, iteration, length tracking (not recommended, choose BigOrderedMap)

Example:

// Table for basic hash storage
let balances = table::new<address, u64>();
table::add(&mut balances, @0x1, 1000);

// SmartTable for efficient storage
let scores = smart_table::new<address, u64>();
smart_table::add(&mut scores, @0x1, 95);

Performance Characteristics

Time Complexity

Worst case

OperationVectorBigVectorSimpleMapOrderedMapBigOrderedMapTableTableWithLengthSmartTable
AppendO(1)O(1)N/AN/AN/AN/AN/AN/A
Insert (index)O(n)O(n)N/AN/AN/AN/AN/AN/A
InsertN/AN/AO(1)O(log(n))O(log(n))O(1)O(1)O(n)
AccessO(1)O(1)O(n)O(log(n))O(log(n))O(1)O(1)O(n)
Delete (end)O(1)O(1)O(1)O(1)O(1)O(1)O(1)O(1)
Delete (index)O(n)O(n)O(1)O(log(n))O(log(n))O(1)O(1)O(1)
IterationO(n)O(n)O(n)O(n)O(n)N/AN/AO(n)

Storage Characteristics

CharacteristicVectorBigVectorSimpleMapOrderedMapBigOrderedMapTableTableWithLengthSmartTable
Storage SlotsO(1)O(n)O(1)O(1)O(n)O(n)O(n)O(n)
ParallelizableNo*SometimesNoNo**SometimesYes*Sometimes*Sometimes
BCS serializableYesNoYesYesNoNoNoNo
  • *Sometimes Table with length based types are parallelizable when there are no new storage slots created.
  • **Sometimes BigOrderedMap is parallelizable when there are no conflicts on insertion to the same storage slot.

Decision Matrix

For Small Datasets (< 1,000 elements)

  • Sequential data: Use Vector
  • Key-value data: Use OrderedMap over SimpleMap unless the data set is really small

For Medium Datasets (1,000 - 10,000 elements)

  • Sequential data: Use BigVector (if scalable)
  • Key-value data: Use Table or BigOrderedMap (if scalable)

For Large Datasets (> 10,000 elements)

  • Sequential data: Use BigVector
  • Key-value data: Use Table (basic), TableWithLength (need length), or BigOrderedMap (storage efficient)

Special Considerations

When You Need Parallelization

  • Use BigVector, BigOrderedMap, Table

When You Need Length Tracking

  • Use Vector, BigVector, OrderedMap, BigOrderedMap, TableWithLength

When You Need Ordering

  • Use Vector, BigVector, OrderedMap, or BigOrderedMap

Best Practices Summary

  1. Start Simple: Begin with standard library collections (Vector, OrderedMap)
  2. Plan for Growth: If your dataset might grow large, consider scalable alternatives early
  3. Consider Storage Costs: Each storage slot has a cost, so choose efficiently
  4. Match Use Case: Choose collections that match your specific requirements
  5. Test Performance: Benchmark with realistic data sizes before committing
  6. Monitor Gas Costs: Different collections have different gas costs for operations
  7. Consider Parallelization: Use parallelizable collections when possible for better performance

Migration Paths

Growing from Small to Large

  • VectorBigVector: When you exceed ~1,000 elements
  • OrderedMapBigOrderedMap: When you exceed ~1,000 entries

Optimizing for Storage

  • TableSmartTable: When you want to reduce storage slot usage
  • TableTableWithLength: When you need length tracking

Optimizing for Performance

  • TableWithLengthTable: When you need parallelization
  • OrderedMapSimpleMap: When you don't need ordering
  • SimpleMap → O(n) lookups mean a lot of reads. Suggested to use OrderedMap instead.
  • SmartTable → There are some ways that the table can become unbalanced, suggested to use BigOrderedMap instead.

This comparison should help you make informed decisions about which collection type to use for your specific use case on Aptos.

Error Handling

Errors on Aptos are fairly simple. Whenever the code aborts, an error message will be returned to the user. The error message is statically defined by the doc comment above it. All error codes are u64. By convention, errors start with E. For example:

/// Uh oh
const E_UH_OH_BAD: u64 = 1;

fun do_something() {
    abort E_UH_OH_BAD
}

In this case, the error message when calling do_something() wil be Uh oh.

More appropriately, errors are usually thrown by asserts. An example here below shows if you want to ensure a value is less than 10.


/// It's too high!
const E_TOO_HIGH: u64 = 128;

fun check(val: u64) {
    assert!(val < 10, E_TOO_HIGH)
}

Additionally, in tests the error code can be omitted for the assertion.

Generic Types

Overview

Generics are similar to other languages, which allow for a function to specify implementation for multiple input types, output types or struct inner types.

Here is a simple example of a parameterized input type:

/// Take a generic type `T` and use it as an input type
fun print<T>(input: &T) {
    std::debug::print(input)
}

Similarly can be used to define outputs:

fun from_string<T>(input: &String): T {
    // Implementation
}

Structs and enums can also have generics in them. Here is an example making a wrapper:

struct Box<T> {
  inner: T
}

We can see in this example, the Box wraps the inner value. This can be used similarly in Enums or in combination with other values.

Note that if a generic is provided in a struct, it will always need to be provided.

Type Constraints

When considering generics, it's important to remember that, it can be any type, and that some consideration has to be made for the differences.

For example, in the previous example, there were no abilities on the types. If you try to drop those types in the function, it will fail to compile, as the input can't be dropped. Here's an example:

fun print<T>(input: T) {
  // ... This will fail with a compilation error
}

But, if we add the drop constraint, only inputs of type T that can be dropped will be allowed. Which provides type safety like so:

fun print<T: drop>(input: T) {
  // ... This will succeed
}

Note that, multiple properties can be added with + like:

fun print<T: copy + drop>(input: T) {
  // ... This will succeed
}

Phantom Generics

The phantom keyword works just like in Rust. It is only used for type checking, but not used for an underlying type. This is useful if you want types that don't conflict, but are essentially the same. Here's an example using Coin.

struct Coin<phantom T> {
  amount: u64
}

The phantom keyword is used, because the T value is used for domain separation, that is that each type of coin is different. But, it is not used directly in the struct's inner values.

Tradeoffs and considerations

Generics as Inputs

Since all types that the constraints apply to, this can be tricky to use when providing into entry functions.

For example if I have a function that takes in a generic and converts it to a single type, we will need to manage all possible types, including primitives to be passed in.

struct Storage {
  bytes: vector<u8>
}

entry fun store_type_as_bytes<T: copy + drop>(caller: &signer) {
  // Convert to bytes
  let bytes: vector<u8> = //...

  move_to(caller, Storage {
    bytes
  })
}

Callers will also have to know which input types are supported, and it would be best to have abort messages explaining that it doesn't support the type.

Example

A perfect example of this is the original Coin standard on Aptos. By using a generic, the caller needs to pass in the coin types. However, the contract will not know all possible types, and cannot store them in the contract.

Generics as Outputs

Using a generic as an output has the same considerations as an input. It can be tricky to properly support different types together in a clean way.

How to Write Tests

Move has a built-in unit testing framework that lets you write tests alongside your smart contract code. Tests are compiled and run by the Move compiler, giving you fast feedback without deploying to a blockchain.

Why Test?

Smart contracts manage real assets and run in an immutable environment. Bugs can be costly and difficult to fix after deployment. Testing helps you:

  • Catch bugs early before deployment
  • Verify correctness of your business logic
  • Document expected behavior through executable examples
  • Enable safe upgrades by ensuring backward compatibility

What You'll Learn

  1. Writing Unit Tests - How to define test functions and use assertions
  2. Running Unit Tests - How to execute tests with the Aptos CLI
  3. Coverage - How to measure test coverage
  4. Formal Verification - Using the Move Prover for mathematical guarantees

Quick Start

Here's a minimal test to give you a sense of the framework:

module my_addr::math {
    public fun add(a: u64, b: u64): u64 {
        a + b
    }

    #[test]
    fun test_add() {
        assert!(add(2, 3) == 5);
        assert!(add(0, 0) == 0);
        assert!(add(100, 200) == 300);
    }
}

Run the test with:

aptos move test --dev

Writing Unit Tests

Unit tests in Move are functions annotated with #[test]. They live inside your modules and have access to all private functions, making it easy to test internal logic.

Basic Test

module my_addr::math {
    fun double(x: u64): u64 {
        x * 2
    }

    #[test]
    fun test_double() {
        assert!(double(0) == 0);
        assert!(double(5) == 10);
        assert!(double(100) == 200);
    }
}

Test Annotations

#[test]

Marks a function as a test. The function will be executed when you run aptos move test.

#[test]
fun test_basic() {
    assert!(1 + 1 == 2);
}

#[test] with Signers

You can create test signers by specifying them in the annotation:

#[test(alice = @0x1, bob = @0x2)]
fun test_with_accounts(alice: &signer, bob: &signer) {
    let alice_addr = std::signer::address_of(alice);
    let bob_addr = std::signer::address_of(bob);
    assert!(alice_addr != bob_addr);
}

#[expected_failure]

Tests that should abort can be marked with #[expected_failure]:

/// Division by zero should abort
const E_DIVIDE_BY_ZERO: u64 = 1;

fun safe_divide(a: u64, b: u64): u64 {
    assert!(b != 0, E_DIVIDE_BY_ZERO);
    a / b
}

#[test]
#[expected_failure(abort_code = E_DIVIDE_BY_ZERO)]
fun test_divide_by_zero() {
    safe_divide(10, 0);
}

You can also specify the location of the abort:

#[test]
#[expected_failure(abort_code = E_DIVIDE_BY_ZERO, location = my_addr::math)]
fun test_divide_by_zero_with_location() {
    safe_divide(10, 0);
}

#[test_only]

Marks a function, import, or constant as available only during testing. Test-only code is excluded from production compilation.

#[test_only]
use aptos_framework::account::create_account_for_test;

#[test_only]
fun setup_test_env(account: &signer) {
    create_account_for_test(std::signer::address_of(account));
}

Testing Resources and Global State

When testing functions that interact with global storage, you typically need to set up accounts:

module my_addr::counter {
    use std::signer;

    struct Counter has key {
        value: u64,
    }

    public entry fun create(account: &signer) {
        move_to(account, Counter { value: 0 });
    }

    public entry fun increment(account: &signer) acquires Counter {
        let addr = signer::address_of(account);
        let counter = &mut Counter[addr];
        counter.value = counter.value + 1;
    }

    #[view]
    public fun get_value(addr: address): u64 acquires Counter {
        Counter[addr].value
    }

    #[test(account = @0x1)]
    fun test_counter(account: &signer) acquires Counter {
        let addr = signer::address_of(account);

        create(account);
        assert!(get_value(addr) == 0);

        increment(account);
        assert!(get_value(addr) == 1);

        increment(account);
        assert!(get_value(addr) == 2);
    }

    #[test(account = @0x1)]
    #[expected_failure]
    fun test_increment_without_create(account: &signer) acquires Counter {
        increment(account); // Should fail: no Counter exists
    }
}

Assertions

Move provides the assert! macro for testing conditions:

#[test]
fun test_assertions() {
    // Basic equality
    assert!(1 + 1 == 2);

    // With error code (useful for debugging)
    assert!(1 + 1 == 2, 0);

    // Boolean conditions
    assert!(true);
    assert!(!false);

    // Comparison
    assert!(10 > 5);
    assert!(5 < 10);
}

Testing Best Practices

  1. Test the happy path: Verify that correct inputs produce correct outputs.
  2. Test error cases: Use #[expected_failure] to verify that invalid inputs are properly rejected.
  3. Test edge cases: Check boundary conditions (zero values, maximum values, empty collections).
  4. Use descriptive test names: Name tests after what they verify (e.g., test_transfer_insufficient_balance).
  5. Set up and tear down: Use #[test_only] helper functions for common setup.
  6. Keep tests focused: Each test should verify one specific behavior.

Running Unit Tests

The Aptos CLI provides commands for running tests, filtering which tests to run, and controlling output verbosity.

Basic Test Execution

Run all tests in your package:

aptos move test

If your package uses named addresses with "_", use the --dev flag to fill them automatically:

aptos move test --dev

Or provide them explicitly:

aptos move test --named-addresses my_addr=0x1

Filtering Tests

Run Tests Matching a Name

aptos move test --filter test_transfer

This runs all tests whose names contain test_transfer.

Run Tests in a Specific Module

aptos move test --filter my_module

Test Output

Successful Output

Running Move unit tests
[ PASS    ] 0x1::counter::test_counter
[ PASS    ] 0x1::counter::test_increment
Test result: OK. Total tests: 2; passed: 2; failed: 0

Failed Output

Running Move unit tests
[ FAIL    ] 0x1::counter::test_bad_assertion
  Error: assertion failed
Test result: FAILED. Total tests: 1; passed: 0; failed: 1

Verbose Output

For more detailed output including gas usage:

aptos move test --dev -v

Debugging with Print

You can print values during tests using std::debug::print:

#[test_only]
use std::debug::print;

#[test]
fun test_with_debug() {
    let value = 42;
    print(&value);  // Prints during test execution
    assert!(value == 42);
}

Run with verbose output to see the printed values:

aptos move test --dev -v

Test Gas Limits

By default, tests have a gas budget. If a test exceeds this limit, it will fail. You can increase the limit:

aptos move test --dev --gas-limit 100000

Common Issues

"Unresolved named address"

Provide the missing address with --named-addresses or use --dev.

"Test exceeded gas limit"

Your test is doing too much computation. Either optimize the test or increase the gas limit.

Flaky tests

Move tests are deterministic -- if a test passes once, it should always pass. If you see inconsistent results, check for uninitialized state or timing-dependent logic.

Coverage

Test coverage measures how much of your code is exercised by tests. The Aptos CLI supports generating coverage reports for your Move packages.

Generating Coverage

Run tests with coverage enabled:

aptos move test --dev --coverage

This runs your tests and generates a coverage report.

Viewing Coverage

After running tests with coverage, view the summary:

aptos move coverage summary --dev

This shows the percentage of code covered by tests for each module.

For a detailed source-level report:

aptos move coverage source --module counter --dev

This shows which lines of code were covered (marked) and which were not.

Interpreting Coverage

  • High coverage (>80%) indicates that most code paths are tested.
  • Low coverage means there are untested code paths that could contain bugs.
  • 100% coverage does not guarantee correctness -- it only means every line was executed.

Focus on testing:

  1. All public entry points
  2. All error conditions
  3. Edge cases and boundary values
  4. State transitions

Best Practices

  1. Aim for high coverage: Target at least 80% coverage for production code.
  2. Cover error paths: Ensure all assert! and abort conditions are tested.
  3. Don't chase 100%: Some code is trivial and doesn't need dedicated tests.
  4. Use coverage to find gaps: Review uncovered lines to identify missing tests.
  5. Run coverage in CI: Make coverage checks part of your continuous integration pipeline.

Formal Verification

Beyond unit testing, Move supports formal verification through the Move Prover. The prover mathematically proves that your code satisfies specified properties, providing stronger guarantees than testing alone.

What is Formal Verification?

Unit tests check specific inputs, but formal verification checks all possible inputs. If the Move Prover verifies a property, it holds for every possible execution path.

Specification Language

Move uses a specification language to express properties that should hold. Specifications are written in spec blocks:

module my_addr::math {
    public fun add(a: u64, b: u64): u64 {
        a + b
    }

    spec add {
        // The result is the sum of the inputs
        ensures result == a + b;
        // The result is at least as large as either input
        ensures result >= a;
        ensures result >= b;
        // Aborts if the addition would overflow
        aborts_if a + b > MAX_U64;
    }
}

Common Specification Constructs

ensures

Describes postconditions -- what must be true after the function executes:

spec get_balance {
    ensures result >= 0;
}

requires

Describes preconditions -- what must be true when the function is called:

spec transfer {
    requires amount > 0;
    requires from_addr != to_addr;
}

aborts_if

Describes under what conditions the function may abort:

spec withdraw {
    aborts_if !exists<Account>(addr);
    aborts_if Account[addr].balance < amount;
}

invariant

Describes properties that must always hold for a struct:

spec module {
    invariant forall addr: address where exists<Counter>(addr):
        Counter[addr].value <= MAX_COUNT;
}

Running the Prover

aptos move prove --dev

The prover will either confirm that all specifications are satisfied or report a counterexample showing a violation.

When to Use Formal Verification

  • Financial logic: Verify that token transfers preserve total supply.
  • Access control: Prove that only authorized accounts can perform privileged operations.
  • Invariants: Ensure that data structure invariants are maintained across all operations.

Limitations

  • The prover may time out on complex functions.
  • Not all Move features are fully supported by the specification language.
  • Writing good specifications requires practice and careful thought.

Best Practices

  1. Start with critical functions: Focus verification on security-sensitive code.
  2. Write specifications incrementally: Add specifications as you develop.
  3. Combine with testing: Use unit tests for basic correctness and formal verification for stronger guarantees.
  4. Keep functions simple: Simpler functions are easier to specify and verify.

Closures, Lambdas, and Function Values

Aptos has extended the Move language with support for closures, lambdas, and function values. These features allow you to pass functions as arguments to other functions, enabling higher-order programming patterns.

Function Types

Move supports function types that describe the signature of a callable:

module my_addr::functional {
    /// Apply a function to each element of a vector
    public fun map(v: &vector<u64>, f: |u64| -> u64): vector<u64> {
        let result = vector[];
        let i = 0;
        let len = v.length();
        while (i < len) {
            result.push_back(f(v[i]));
            i = i + 1;
        };
        result
    }
}

Lambda Expressions

Lambdas are anonymous functions defined inline:

fun example() {
    let numbers = vector[1, 2, 3, 4, 5];
    let doubled = map(&numbers, |x| x * 2);
    // doubled = [2, 4, 6, 8, 10]
}

Using with Standard Library

The standard library provides functions that accept closures:

fun example() {
    let numbers = vector[1, 2, 3, 4, 5];

    // for_each applies a function to each element
    numbers.for_each(|n| {
        std::debug::print(&n);
    });

    // filter keeps elements matching a predicate
    let evens = numbers.filter(|n| *n % 2 == 0);
}

Inline Functions with Closures

Inline functions that accept closures allow the compiler to optimize away the function call overhead:

public inline fun do_if<T>(condition: bool, f: || -> T, default: T): T {
    if (condition) {
        f()
    } else {
        default
    }
}

Limitations

Closures in Move have some restrictions compared to other languages:

  • Closures cannot capture mutable references from the enclosing scope in all cases.
  • Function values cannot be stored in structs or global storage.
  • Recursive closures are not supported.

These restrictions exist to maintain Move's safety guarantees while providing the expressiveness of higher-order functions.

When to Use Closures

  • Collection processing: Mapping, filtering, and folding over vectors.
  • Custom iteration: Providing callback functions for iteration patterns.
  • Configuration: Passing behavior as a parameter to generic functions.

Events

Events are the way that allows apps and users to easily see what's going on in transactions. For all transactions, a WriteSet is created, which keeps track of which changes to the blockchain storage occurred in the transaction.

Events can also be emitted, which are custom to the developer's choices. Let's dive into some of the details.

Defining and Emitting Events

Choosing your events

There are two types of events: handle events and module events.

Note that, handle events are mostly deprecated except in some situations, and are not parallelizable. For this reason, I will only go over module events in this book.

Module Events

To define a module event in Move, you simply need to add a #[event] annotation above the struct you want to be an event. Then call emit for each one you want to emit. Here's an example:

module my_addr::my_module {
    use std::signer;
    use std::string::String;

    #[event]
    struct Message {
        caller: address,
        inner: String
    }

    entry fun emit_message(caller: &signer, message: String) {
        aptos_framework::event::emit(Message {
            caller: signer::address_of(caller),
            inner: message
        })
    }
}

This will emit a message, which will show up in the writeset for later indexing.

Note that the events do not have a sequence number.

Indexing Events

Once events are emitted on-chain, they need to be consumed by off-chain services. This is done through indexing -- the process of reading events from the blockchain and storing them in a queryable database.

How Events Are Stored

When a transaction emits events, they are included in the transaction output and stored on the blockchain. Each event contains:

  • The event type (the fully qualified struct name)
  • The event data (the serialized struct fields)
  • The sequence number (order within the transaction)

Querying Events

Using the Aptos REST API

You can query events through the Aptos Node API:

GET /v1/accounts/{address}/events/{event_handle}/{field_name}

For module events (the modern #[event] style), you can query by event type:

GET /v1/events?event_type={module_address}::{module_name}::{EventStructName}

Using the Aptos Indexer

The Aptos Indexer provides a GraphQL API for more complex event queries:

query GetTransferEvents {
  events(
    where: {
      type: { _eq: "0x1::coin::WithdrawEvent" }
    }
    order_by: { transaction_version: desc }
    limit: 10
  ) {
    type
    data
    transaction_version
    event_index
  }
}

Using the TypeScript SDK

The Aptos TypeScript SDK provides methods for querying events:

import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";

const config = new AptosConfig({ network: Network.MAINNET });
const aptos = new Aptos(config);

const events = await aptos.getModuleEventsByEventType({
  eventType: "0x1::coin::WithdrawEvent",
});

Building an Indexer

For production applications, you typically run a custom indexer that:

  1. Streams transactions from an Aptos fullnode.
  2. Filters for events relevant to your application.
  3. Processes the event data and stores it in a database.
  4. Serves the indexed data through an API.

The Aptos Indexer framework provides tools for building custom processors:

  • Transaction Stream Service: Streams raw transaction data.
  • Custom Processors: Parse and index specific event types.
  • PostgreSQL Storage: Store indexed data for querying.

Best Practices

  1. Design events for indexing: Include all data that off-chain services will need.
  2. Use consistent event naming: Make event types easy to filter and query.
  3. Index critical events: Ensure all important state changes are indexed.
  4. Handle reorgs: Your indexer should handle chain reorganizations gracefully.
  5. Monitor indexer health: Set up alerts for indexing lag or failures.

Parallelization

The Aptos blockchain was built originally to provide Facebook level TPS for the future of the internet. This means to achieve this scale, parallelization of execution takes a big play.

We'll go over how parallelization works on Aptos, and how to code to take advantage of this.

Parallelization Basics

BlockSTM is the parallel execution engine that drives Aptos. It uses what we call dynamic (or optimistic) parallelism. What this means is that all transactions are executed in parallel, and the write sets between each are compared. If they write (and then read or write) to the same storage slot, it's a conflict, and therefore those transactions must be serialized.

How BlockSTM Works

  1. Optimistic Execution: All transactions in a block are executed in parallel, assuming no conflicts. When all transactions complete without conflicts, the entire block can be committed without any re-execution -- this is the ideal case that BlockSTM is optimized for.
  2. Validation: After execution, the read/write sets of each transaction are compared.
  3. Conflict Detection: If transaction B reads a storage slot that transaction A wrote to, and A comes before B in the block ordering, this is a conflict.
  4. Re-execution: Conflicting transactions are re-executed with the updated state.
  5. Commitment: Once all transactions are validated, the block is committed.

This approach achieves near-linear scaling with the number of CPU cores when transactions access independent state, which is the common case for most blockchain workloads.

Parallelization Considerations

When considering code on Aptos for parallelization, think of these three things:

  1. Where will the most conflicts occur on my codebase?
  2. Can I use different accounts or addresses to ensure that there are reduced conflicts?
  3. Can I use separate storage slots (such as a table or objects) to ensure that there are reduced conflicts?
  4. Can I use an aggregator instead for my conflicting read / writes to a single number?

Design Patterns

Design patterns are reusable solutions to common problems in Move smart contract development. This chapter covers patterns that have emerged from the Aptos ecosystem and are considered best practices.

Patterns Covered

  1. Account Authorization by Signer - Using signers to control access to resources
  2. Event Emission - Emitting events for indexing and observability

Why Patterns Matter

Smart contracts are immutable once deployed and manage real assets. Using well-tested patterns helps you:

  • Avoid common security pitfalls: Patterns encode lessons learned from real-world exploits.
  • Write idiomatic code: Other developers can understand your code more easily.
  • Build reliable contracts: Patterns have been battle-tested across many projects.

Account Authorization by Signer

The signer is the primary mechanism for authorization in Move. A signer value can only be created by the Aptos VM as part of a transaction -- it cannot be forged. This makes signers the foundation for access control in smart contracts.

The Signer Type

A signer represents the identity of an account that signed a transaction. It is used to:

  1. Authorize resource creation: move_to requires a signer to store resources under an address.
  2. Prove identity: Functions can verify the caller's address.
  3. Control access: Only the signer can modify their own resources (unless explicitly delegated).

Basic Pattern

module my_addr::profile {
    use std::signer;
    use std::string::String;

    /// Only the account owner can modify their profile
    struct Profile has key {
        name: String,
        bio: String,
    }

    /// Create a profile -- requires the signer's authorization
    public entry fun create_profile(
        account: &signer,
        name: String,
        bio: String,
    ) {
        move_to(account, Profile { name, bio });
    }

    /// Update a profile -- only the owner can call this
    public entry fun update_bio(
        account: &signer,
        new_bio: String,
    ) acquires Profile {
        let addr = signer::address_of(account);
        let profile = &mut Profile[addr];
        profile.bio = new_bio;
    }
}

The signer ensures that only the account owner can create or modify their own profile.

Authorization Checks

Verifying the Caller

const E_NOT_ADMIN: u64 = 1;
const ADMIN_ADDRESS: address = @0x1;

public entry fun admin_only_action(account: &signer) {
    let addr = signer::address_of(account);
    assert!(addr == ADMIN_ADDRESS, E_NOT_ADMIN);
    // Perform admin action
}

Multi-Signer Authorization

Entry functions can require multiple signers, which enables multi-party authorization:

public entry fun joint_action(
    party_a: &signer,
    party_b: &signer,
) {
    let addr_a = signer::address_of(party_a);
    let addr_b = signer::address_of(party_b);
    // Both parties have authorized this action
}

Signer vs. Address

A common question is when to use &signer versus address as a function parameter:

  • Use &signer when the function modifies the caller's state or needs authorization.
  • Use address when the function only reads state.
/// Requires authorization -- uses signer
public entry fun deposit(account: &signer, amount: u64) { ... }

/// Read-only -- uses address
#[view]
public fun get_balance(addr: address): u64 { ... }

Capability Pattern

For delegated authorization, you can use a capability pattern where a signer creates a capability object that can be used later:

module my_addr::managed_token {
    use std::signer;

    struct MintCapability has key, store {
        max_amount: u64,
    }

    /// Only the admin can grant mint capabilities
    public entry fun grant_mint_capability(
        admin: &signer,
        recipient: &signer,
        max_amount: u64,
    ) {
        assert!(signer::address_of(admin) == @my_addr, 1);
        move_to(recipient, MintCapability { max_amount });
    }

    /// Anyone with a MintCapability can mint
    public entry fun mint(account: &signer, amount: u64) acquires MintCapability {
        let addr = signer::address_of(account);
        let cap = &MintCapability[addr];
        assert!(amount <= cap.max_amount, 2);
        // Perform minting
    }
}

Best Practices

  1. Always use signers for state changes: Never accept a plain address for operations that modify state.
  2. Check addresses explicitly: When you need a specific caller (like an admin), verify the address with assert!.
  3. Minimize signer scope: Pass &signer (reference) rather than signer (by value) to prevent unintended transfers.
  4. Use capabilities for delegation: When you need to delegate authority, create capability resources rather than passing signers around.

Event Emission

Events are the primary mechanism for making contract state changes observable to off-chain services like indexers, wallets, and dApps. Well-designed events make your contract easy to integrate with.

The Event Pattern

Define the Event

Events are structs with the #[event] attribute and the drop and store abilities:

module my_addr::marketplace {
    use aptos_framework::event;

    #[event]
    struct ItemListed has drop, store {
        seller: address,
        item_id: u64,
        price: u64,
    }

    #[event]
    struct ItemSold has drop, store {
        seller: address,
        buyer: address,
        item_id: u64,
        price: u64,
    }
}

Emit the Event

Use event::emit to emit events during function execution:

public entry fun list_item(
    seller: &signer,
    item_id: u64,
    price: u64,
) {
    let seller_addr = signer::address_of(seller);

    // Business logic...

    event::emit(ItemListed {
        seller: seller_addr,
        item_id,
        price,
    });
}

public entry fun buy_item(
    buyer: &signer,
    seller_addr: address,
    item_id: u64,
) {
    let buyer_addr = signer::address_of(buyer);
    let price = get_item_price(seller_addr, item_id);

    // Transfer logic...

    event::emit(ItemSold {
        seller: seller_addr,
        buyer: buyer_addr,
        item_id,
        price,
    });
}

What to Include in Events

Good events contain enough information for off-chain consumers to understand what happened:

  • Who: The addresses of the parties involved.
  • What: The specific action that occurred.
  • Details: Relevant data like amounts, IDs, and timestamps.
#[event]
struct Transfer has drop, store {
    from: address,
    to: address,
    amount: u64,
}

Event Design Guidelines

  1. One event per significant action: Emit an event for every state change that external observers might care about.
  2. Include all relevant data: Don't force indexers to make additional API calls to reconstruct what happened.
  3. Use descriptive names: Event struct names should clearly describe the action (e.g., TokenMinted, OrderFilled).
  4. Keep events small: Don't include large data structures -- include IDs and references instead.
  5. Be consistent: Use consistent naming and field ordering across all events in your module.

Testing Events

You can verify event emission in tests:

#[test(seller = @0x1)]
fun test_list_emits_event(seller: &signer) {
    list_item(seller, 1, 100);
    // The event is emitted during execution
    // Indexers will pick it up from the transaction output
}

Best Practices

  1. Always emit events for entry functions: Any user-facing action should emit an event.
  2. Don't emit events for view functions: View functions don't change state.
  3. Use module events (not handle events): The #[event] attribute with event::emit is the modern approach.
  4. Document your events: Include doc comments on event structs so consumers know what each field means.

Data Models

One of the important concepts of Aptos is flexibility. Aptos provides multiple data models to allow users to pick and choose the model that is best for their use case, or even mix and match them accordingly.

Move on Aptos is built around two base data models. To read more about them, see each page:

The Account Model

The Account model of Aptos behaves where each user has their data stored in global storage. Think of this as a giant mapping between the set of Address and Resource Name to a single storage slot.

TODO: Diagram

There are two types of accounts today:

User Accounts

User accounts are the standard accounts that users create to interact with the Aptos blockchain. They are denoted by the resource 0x1::account::Account and are used to hold assets, execute transactions, and interact with smart contracts. They are generated from a signer and are associated with a public key. The public key is then hashed to create the account address.

Resource Accounts

Resource accounts are accounts that are separate of a user account. The accounts are derived from an existing account, and can have a SignerCapability stored in order to sign as the account. Alternatively, the signer can be rotated to 0x0 preventing anyone from authenticating as an account.

The Object Model

Objects similar to resource accounts, but rather than using a SignerCapability instead a ExtendRef can be used to authenticate for the account. These have owners, and always have the resource 0x1::object::Object stored at its address.

TODO: Diagram

graph
  subgraph Account
    0x1::account::Account
  end
  subgraph Object
    subgraph 0x1::object::ObjectCore
      Owner
      
    end
    A[0x1::object::ObjectCore]
    B[0x42::example::Resource]
    C[0x1234::example2::Resource2]
  end
  
  Owner --> 0x1::account::Account

Data Model Tradeoffs

You probably are asking, when would I use one over the other? Here are some details of advantages and disadvantages of each.

Account Model

The account model is very simple where each address has direct ownership over its resources. The resources at that address can only be added with that signer. Keep in mind, we'll mention both user and resource accounts below.

Account Model Advantages

  • User accounts are simple and directly tied to a key.
  • Resource accounts are similar to contract addresses in Eth, and can have programmatic access.
  • Only the signer associated with the key can write data to the account.
  • All resources are indexed by account, and type. Easily accessed automatically in transactions by the signer.
  • Creator control over how resources in an account are accessed.
  • Ownership based indexing is simple, the account containing the resources is the owner.

Account Model Disadvantages

  • Parallelism is not as easy, requires to ensure that multiple accounts don't access a shared resource.
  • No programmatic access except for resource accounts.
  • No way to get rid of resources in an account, except through the original contract.

Object Model

The object model also has each address has resources owned by the owner of the object. This helps provide more complex ownership models, as well as some tricks for providing composability, and soul-bound resources.

Object Model Advantages

  • Parallelism across objects are easy, just create separate objects for parallel tasks.
  • Built in ownership.
  • Resources are collected easily in a resource group.
  • With the resource group, all resources in the group get written to the write set.
  • Multiple resources in the resource group only cause a single storage read (less gas).
  • Addresses can be randomly generated or derived from the initial owner for instant access.
  • Programmatic signer access.
  • Composability is easy, NFTs own other NFTs etc.
  • Creator control over ownership, transfers, and other pieces.
  • Owner can choose to hide the object, allowing wallets or other items to hide it.

Object Model Disadvantages

  • For full parallelism, addresses need to be stored off-chain and passed into functions,
  • Keeping track of objects can be complex.
  • More complex access, does require handling ownership or other access actions.
  • Soul-bound objects cannot be removed entirely, indexers need to ignore the resources to make them disappear.
  • More complex indexing needed to keep track of object owners and properties (especially with ownership chains).

Advanced Topics

This section delves into specialized concepts and powerful features for experienced Aptos developers.

Here, you'll explore:

  • Binary Canonical Serialization (BCS): Understand the core serialization format used by Aptos.
  • Storage: Learn about on-chain and off-chain data management strategies.
  • Gas: Optimize your contracts by understanding the gas model for execution, I/O, and storage.

Mastering these topics will help you build more efficient, robust, and cost-effective applications on the Aptos blockchain.

Binary Canonical Serialization (BCS)

Binary Canonical Serialization (BCS) is a method that allows for compact and efficient storage. It was invented at Diem for the purposes of a consistent signing mechanism.

Properties

Binary

The serialization method is directly in bytes and is not human-readable. For example, for a String hello, it would be represented by the length of the string as a binary encoded uleb-128, followed by the UTF-8 encoded bytes of hello.
e.g. "hello" = 0x0548656C6C6F This is different from, say, a human-readable format such as JSON which would give "hello"

Canonical

There is only one canonical way to represent the bytes. This ensures that signing and representation are consistent.

Example:

Let's consider this struct in Move:

module 0x42::example {
    struct FunStruct {
        a: u8,
        b: u8
    }
}

In JSON, the struct {"a":1, "b": 2} can also be represented as {"b":2, "a":1}. Both are interchangeable so they are not canonical. In BCS, it would be a pre-defined order, so only one would be the valid representation.

However, in BCS, there is only one valid representation of that, which would be the bytes 0x0102. 0x0201 is not canonical, and it would instead be interpreted as {"a":2, "b":1}.

Non-self describing

The format is not self-describing. This means that deserialization requires knowledge of the shape and how to interpret the bytes. This is in opposition to a type like JSON, which is self-describing.

Example:

Let's consider this struct in Move again:

module 0x42::example {
    struct FunStruct {
        a: u8,
        b: u8
    }
}

The first byte will always be interpreted as a then b. So, 0x0A00 would be {"a":10, "b":0} and 0x0A01 would be {"a":10, "b":1}. If we flip it to 0x000A it would be {"a":0, "b":10}.

Note that this means if I do not know what the shape of the struct is, then I do not know if this is a single u16, the above struct, or something else.

Details about different types

BCS Primitives

Here is a list of primitives, and more descriptions below. Keep in mind all numbers are stored in little-endian byte order.

TypeNumber of bytesDescription
bool1Boolean value (true or false)
u81Unsigned 8-bit integer
u162Unsigned 16-bit integer
u324Unsigned 32-bit integer
u648Unsigned 64-bit integer
u12816Unsigned 128-bit integer
u25632Unsigned 256-bit integer
address32Aptos Address (32-byte integer)
uleb1281-32Unsigned little-endian base-128 integer

Bool

A boolean is a single byte. 0x00 represents false, 0x01 represents true. All other values are defined as invalid.

Examples:

ValueBCS Serialized Value
false0x00
true0x01

U8

A U8 is an unsigned 8-bit integer (1 byte).

Examples:

ValueBCS Serialized Value
00x00
10x01
160x0F
2550xFF

U16

A U16 is an unsigned 16-bit integer (2 bytes).

Examples:

ValueBCS Serialized Value
00x0000
10x0001
160x000F
2550x00FF
2560x0100
655350xFFFF

U32

A U32 is an unsigned 32-bit integer (4 bytes).

Examples:

ValueBCS Serialized Value
00x00000000
10x00000001
160x0000000F
2550x000000FF
655350x0000FFFF
42949672950xFFFFFFFF

U64

A U64 is an unsigned 64-bit integer (8 bytes).

Examples:

ValueBCS Serialized Value
00x0000000000000000
10x0000000000000001
160x000000000000000F
2550x00000000000000FF
655350x000000000000FFFF
42949672950x00000000FFFFFFFF
184467440737095516150xFFFFFFFFFFFFFFFF

U128

A U128 is an unsigned 128-bit integer (16 bytes).

Examples:

ValueBCS Serialized Value
00x00000000000000000000000000000000
10x00000000000000000000000000000001
160x0000000000000000000000000000000F
2550x000000000000000000000000000000FF
655350x0000000000000000000000000000FFFF
42949672950x000000000000000000000000FFFFFFFF
184467440737095516150x0000000000000000FFFFFFFFFFFFFFFF
3402823669209384634633746074317682114550xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

U256

A U256 is an unsigned 256-bit integer (32 bytes).

Examples:

ValueBCS Serialized Value
00x0000000000000000000000000000000000000000000000000000000000000000
10x0000000000000000000000000000000000000000000000000000000000000001
160x000000000000000000000000000000000000000000000000000000000000000F
2550x00000000000000000000000000000000000000000000000000000000000000FF
655350x000000000000000000000000000000000000000000000000000000000000FFFF
42949672950x00000000000000000000000000000000000000000000000000000000FFFFFFFF
184467440737095516150x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF
3402823669209384634633746074317682114550x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
1157920892373161954235709850086879078532699846656405640394575840079131296399350xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

Address

An address is the 32-byte representation of a storage slot on Aptos. It can be used for accounts, objects, and other addressable storage.

Addresses, have special addresses 0x0 -> 0xA, and then full length addresses. Note, for legacy purposes, addresses missing 0's in front, are extended by filling the missing bytes with 0s.

Examples:

ValueBCS Serialized Value
0x00x0000000000000000000000000000000000000000000000000000000000000000
0x10x0000000000000000000000000000000000000000000000000000000000000001
0xA0x000000000000000000000000000000000000000000000000000000000000000A
0xABCDEF (Legacy shortened address)0x0000000000000000000000000000000000000000000000000000000000ABCDEF
0x0000000000000000000000000000000000000000000000000000000000ABCDEF0x0000000000000000000000000000000000000000000000000000000000ABCDEF
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

Uleb128

A uleb128 is a variable-length integer used mainly for representing sequence lengths. It is very efficient at storing small numbers, and takes up more space as the number grows. Note that the Aptos specific implementation only supports representing a u32 in the uleb128. Any value more than the max u32 is considered invalid.

Examples:

ValueBCS Serialized Value
00x00
10x01
1270x7F
1280x8001
2400xF001
2550xFF01
655350xFFFF03
167772150xFFFFFF07
42949672950xFFFFFFFF0F

Sequences

Sequences are represented as an initial uleb128 followed by a sequence of encoded types. This can include sequences nested inside each other. You can compose them together to make more complex nested vectors

Detailed Example:

The most trivial example is an empty sequence, which is always represented as the zero length byte 0x00. This is for any sequence no matter the type.

A more complex example is the vector<u8> [0, 1, 2]. We first encode the length, as a uleb128, which is the byte 0x03. Then, it is followed up by the three individual u8 bytes 0x00, 0x01, 0x02. This gives us an entire byte array of 0x03000102.

Examples:

TypeValueEncoded Value
vector<u8>[]0x00
vector<u8>[2]0x0102
vector<u8>[2,3,4,5]0x0402030405
vector<bool>[true, false]0x020100
vector<u16>[65535, 1]0x02FFFF0001
vector<vector<u8>>[[], [1], [2,3]]0x03000101020203
vector<vector<vector<u8>>>[[[],[1]],[],[[2,3],[4,5]]]0x03020001010002020203020405

Longer examples (multi-byte uleb128 length):

TypeValueEncoded Value
vector<u8>[0,1,2,3,...,126,127]0x8001000102...FDFEFF
vector<u32>[0,1,2,...,4294967293,4294967294,4294967295]0xFFFFFFFF0F0000000000000001...FFFFFFFEFFFFFFFFF

Structs

Structs are represented as an ordered list of bytes. They must always be in this same order. This makes it very simple to always interpret the bytes in a struct.

Detailed example:

Consider the following struct:

module 0x42::example {
    struct ExampleStruct {
        number: u8,
        vec: vector<bool>,
        uint16: u16
    }
}

We see here that we have mixed types. These types will always be interpreted in that order, and must be canonical.

Here is an example of the struct:

ExampleStruct {
  number: 255,
  vec: vector[true, false, true],
  uint16: 65535
}

This would be encoded as each of the individual encodings in order.

  • 255 = 0xFF
  • [true, false, true] = 0x03010001
  • 65535 = 0xFFFF

So combined they would be: 0xFF03010001FFFF

Enums

Enums allow for upgradable and different types in a compact representation. They are headed first by a type (in a uleb128), followed by the expected type values.

Example:

Here is an enum in Move, you can see the first value is a struct, the second is a simple value, and the third is a tuple.

module 0x42::example {
    enum ExampleStruct {
        T1 {
            number: u8,
            vec: vector<bool>,
            uint16: u16
        },
        T2,
        T3(u8, bool)
    }
}

Struct Enums

Let's start with the first type:

ExampleStruct::T1 {
  number: 1,
  vec: [true, false, true]
  uint16: 65535
}

This would first start with the initial uleb128 representing the type, then followed by the bytes. In this case, it is the first in the enum, so it will be represented as enum 0. All together it is represented by: 0x000103010001FFFF.

To use the struct enum in a match statement, you can do the following:

module 0x42::example {
    enum ExampleStruct has drop {
        T1 {
            number: u8,
            vec: vector<bool>,
            uint16: u16
        },
        T2,
        T3(u8, bool)
    }

    public fun handle_example(example: ExampleStruct): u8 {
        match (example) {
            ExampleStruct::T1 { number, vec: _, uint16 } => {
                // Do something with the struct fields
                return number + uint16 as u8; // Just an example operation
            }
            _ => {
                abort 1 // Handle other cases
            }
        }
    }
}

Simple Value Enums

For the second type, it's simply just represented as the uleb128 representing the type for value 1. This is useful for traditional enums that do not have any additional data. In this case, the enum is T2, which has no fields, and can be represented as:

ExampleStruct::T2 {} = 0x01

To use the simple value enum in a match statement, you can do the following:

module 0x42::example {

    enum ExampleStruct has drop {
        T1 {
            number: u8,
            vec: vector<bool>,
            uint16: u16
        },
        T2,
        T3(u8, bool)
    }

    public fun handle_example(example: ExampleStruct): u8 {
        match (example) {
            ExampleStruct::T2 => {
                return 42; // some arbitrary value for T2
            }
            _ => {
                abort 1
            }
        }
    }
}

Tuple Enums

For the third type, it's represented as the uleb128 representing the type for value 2 followed by the tuple. The tuple can contain any types, and they will be encoded in the same way as structs, just without named fields. In this case, the tuple is (3, true), and can be represented as:

ExampleStruct::T2(3,true) = 0x020301

To use the tuple enum in a match statement, you can do the following:

module 0x42::example {
    enum ExampleStruct has drop {
        T1 {
            number: u8,
            vec: vector<bool>,
            uint16: u16
        },
        T2,
        T3(u8, bool)
    }

    public fun handle_example(example: ExampleStruct): (u8, bool) {
        let (start, end) = match (example) {
            ExampleStruct::T3(x, y) => {
                (x, y)
            }
            _ => {
                abort 1
            }
        };

        return (start, end)
    }
}

Strings

Strings are a special case. They are represented as a vector<u8>, but only UTF-8 valid characters are allowed. This means, it would look exactly the same as a vector. Note, that the sequence length is the number of bytes, not the number of characters.

For example the string: ❤️12345 = 0x0BE29DA4EFB88F3132333435

Examples:

ValueEncoded Value
A0x0141
hello0x0568656C6C6f
goodbye0x07676F6F64627965
❤️0x06e29da4efb88f
💻0x03E29C8D

Options

Options are a special case of enums. It is simply either a 0 for a None option and a 1 plus the value for a Some option. The purpose of the option is to represent a value that may or may not be present, similar to other programming languages' Option or Maybe types.

Examples:

TypeValueEncoded Value
OptionNone0x00
OptionSome(false)0x0100
OptionSome(true)0x0101
Option<vector<u16>>None0x00
Option<vector<u16>>Some([1,65535))0x01020001FFFF

Storage

Aptos currently uses RocksDB for on-chain storage. The data is stored in mainly addressable storage on-chain, but the Digital Assets Standard uses off-chain storage for image and other assets.

Types of storage

On-chain Storage

On-chain Rocks DB uses a concept of storage slots. The storage slots are accessible storage, and each slot has an associated cost with it. The slots are accessible by a hash of given inputs.

There are a few types:

  • Resources
  • Resource Groups
  • Tables
  • Vectors
  • BigVectors
  • SimpleMap
  • OrderedMap
  • BigOrderedMap

Resources

Resource storage a single slot is the combination of address and resource name. This combination gives a single slot, which storing a lot of data in that resource, can be efficient.

TODO: Diagram

Keep in mind that the storage deposit associated with a resource is a single slot per resource.

Resource Groups

As specified in AIP-9, resource groups are a collection of resources in a single storage slot. These are stored as a b-tree, in the storage slot, which allows for more efficient packing of data (fewer slots). This is a slight tradeoff where, if accessing multiple resources together often, will be much more efficient, but all writes send all resources in the group to the output writeset. There is also slight cost to navigate the b-tree.

TODO: Diagram

The most common usage for resource groups is for the 0x1::object::ObjectGroup group for objects.

Tables

Tables are hash addressable storage based on a handle stored in a resource. Each item in a table is a single storage slot, but with the advantage that has less execution cost associated. Additionally, each storage slot can be parallelized separately. Note that by far tables are the most expensive, as you need to store both the slot for the handle and the slot for each individual table item. The basic table handle cannot be deleted, but the table items can be. The cost of the table handle's slot cannot be recovered via storage refund.

TODO: Diagram

Note that there is no indexed tracking of which table items are filled or not and how many there are, this must be done off-chain or with a different table variant.

There are other variants of the table with different tradeoffs:

  • Table with length
  • Smart Table

Table With Length

Table with length is exactly the same as a table, but with a length value. Keep in mind that table with length can be deleted fully including the table handle. However, table with length is not parallelizable on creation or deletion of table items, because every transaction increments or decrements the length.

TODO: Diagram

Smart Table

Smart table uses buckets to lower the number of storage slots used. It keeps track of the length, and it buckets items into vectors. It is additionally iterable over the course of the whole table.

TODO: Diagram

Note: there is a possible DDoS vector if people can create keys that end up in a single storage item.

Vector

Vectors are sequences of

Off-Chain Storage

All URLs stored on Aptos can be of the following:

  • An HTTP or HTTPS URL e.g. https://mydomain.com/image.png
  • An IPFS URL e.g. ipfs://hash

TODO: Followup with more info / examples

Indexing

Off-chain indexing is also very common, see https://aptos.dev for more info.

TODO: Link tutorials and other information.

Gas

Gas is used to measure the amount of execution, storage, and IO used for every transaction. This is to provide fairness and ensure the network runs smoothly and with high performance. Gas cost for a transaction has three parts:

  • The gas used - which is the amount of units used to execute the transaction (execution, storage, and IO).
  • The gas unit price (also sometimes called price per unit gas) - which is the amount the fee payer chose to pay to prioritize the transaction.
  • The storage refund - which is based on the number of storage slots deleted.

The total gas cost (fee or refund) is calculated as:

Keep in mind, the storage refund can be greater than the other side, so you can actually gain gas in a transaction by freeing storage slots.

How is gas used calculated?

Gas used is calculated by three parts:

  1. The number of execution units, which vary based on the operations taken.
  2. The number of IO units, which vary based on which storage slots are read or written.
  3. The storage deposit, which is the cost for each new storage slot created. Storage deposit is returned to users.

Each one of these has an individual upper bound, so keep that in mind if you have a task that uses any one of these heavily.

Execution

Execution gas is measured by the amount of work that an operation does on-chain. Keep in mind that this includes things like:

  • Iterating over items in a table
  • Performing cryptographic operations
  • Unpacking input arguments
  • Mathematical operations
  • Control flow operations

IO

IO operations are the amount of reads and writes done to existing storage slots (or storage slots after they are created). This includes things like:

  • Writing to existing storage
  • Adding values to a vector and saving it in storage
  • Deleting values from a vector and saving it in storage
  • Reading existing storage slots

Storage Deposit / Refund

Storage deposit (and subsequent refund) are based on the number of storage slots created or deleted. Each storage slot has a cost associated with it, which is deposited to encourage freeing of storage slots later. The storage slots that are freed will then refund the fee payer of the transaction, possibly even making their transaction be free or even pay them for the transaction.

TODO: More details

Configuring Gas

There are only 2 knobs you can turn today on Aptos to configure gas cost for a transaction.

  • Max gas amount - The number of gas units you are willing to spend on a transaction.
  • Gas unit price - The number of octas (APT*10^-8) from a minimum of 100.

Max gas amount

If a max gas amount is too low for a transaction to complete, OUT_OF_GAS will be returned, and the transaction will abort. This means that nothing will happen in the transaction, but you will still pay the gas. Setting this to a reasonable bound prevents you from spending too much on a single transaction.

Range: 2 - ??? (TODO: Put number or how to get it from gas config)

If you do not have enough APT to fulfill the gas deposit (max gas amount * gas unit price), you will get an error of INSUFFICIENT_BALANCE_FOR_TRANSACTION_FEE. This means you will need more APT in your account, or to adjust one of these two values.

Gas unit price

Gas unit price is the amount you're willing to pay per gas unit on a transaction. Higher values are prioritized over lower values. When choosing a gas unit price, keep in mind that your account needs enough APT to pay for the entire max gas amount times the gas unit price.

Range: 100 - ??? (TODO: put number or how to get it from gas config)

Gas Config

TODO: Explain how to get it directly from on-chain.

Standard Libraries

Aptos provides multiple standard libraries at the given addresses:

  • 0x1
  • 0x3
    • AptosToken - Legacy NFT standard (not suggested for any future usage)
  • 0x4
    • AptosTokenObjects - Digital Assets -> New NFT and semi-fungible token standard

Standards

These standards are built into the 0x1 and 0x4 addresses:

Additional Libraries

The Aptos ecosystem also has third-party libraries and tools:

Coin (Legacy)

Coin is the original standard for fungible tokens on Aptos. It is being replaced with the fungible asset standard, which allows for much more flexibility and more fine-grained control over the fungible tokens. Note that APT is a Coin being migrated to the fungible asset.

TODO: Simple example

Migrating coins to Fungible asset

TODO

Fungible Assets

The fungible asset standard is a fungible token standard built for flexibility and control over supplies and their properties. Note that this is built upon the object model.

Dispatchable Fungible Assets

TODO: More details

Digital Assets

The digital asset standard is a token standard for both non-fungible and semi-fungible tokens. It's important to note that combining the fungible asset standard and the digital asset standard create semi-fungible tokens. Note that this is built upon the object model.

Move Standard Library (MoveStdlib)

The Move Standard Library (std) provides the foundational types and utilities used across all Move programs. It is deployed at address 0x1.

Source: aptos-framework/move-stdlib

Key Modules

std::string

UTF-8 encoded strings.

use std::string::{Self, String};

let greeting: String = string::utf8(b"Hello, Aptos!");
let length = string::length(&greeting);
let is_empty = string::is_empty(&greeting);

std::vector

Dynamic arrays (see Collections > Vectors for details).

use std::vector;

let v = vector[1, 2, 3];
let len = v.length();

std::option

Optional values -- a value that may or may not be present.

use std::option::{Self, Option};

let some_val: Option<u64> = option::some(42);
let none_val: Option<u64> = option::none();
let val = option::extract(&mut some_val); // 42

std::signer

Functions for working with transaction signers.

use std::signer;

fun get_addr(account: &signer): address {
    signer::address_of(account)
}

std::error

Standard error categories for structured error reporting.

use std::error;

const E_NOT_FOUND: u64 = 1;

fun example() {
    abort error::not_found(E_NOT_FOUND)
}

Error categories include: not_found, already_exists, permission_denied, invalid_argument, invalid_state, out_of_range, resource_exhausted, and internal.

std::bcs

Binary Canonical Serialization (see BCS chapter).

use std::bcs;

let bytes = bcs::to_bytes(&42u64);

std::hash

Cryptographic hash functions.

use std::hash;

let data = b"hello";
let hash = hash::sha3_256(data);

std::debug

Debugging utilities (test-only).

#[test_only]
use std::debug;

#[test]
fun test_debug() {
    debug::print(&42);
}

std::fixed_point32 / std::fixed_point64

Fixed-point arithmetic for precise financial calculations.

use std::fixed_point32::{Self, FixedPoint32};

let half = fixed_point32::create_from_rational(1, 2);
let result = fixed_point32::multiply_u64(100, half); // 50

Summary

ModulePurpose
std::stringUTF-8 strings
std::vectorDynamic arrays
std::optionOptional values
std::signerTransaction signer utilities
std::errorStructured error codes
std::bcsSerialization
std::hashCryptographic hashing
std::debugTest-time debugging
std::fixed_point32/64Fixed-point math

Aptos Standard Library (AptosStdlib)

The Aptos Standard Library (aptos_std) extends the Move Standard Library with data structures and utilities specific to the Aptos blockchain. It is deployed at address 0x1.

Source: aptos-framework/aptos-stdlib

Key Modules

Data Structures

aptos_std::smart_table

A scalable hash table designed for large datasets. Entries are stored in separate storage slots for parallelism.

use aptos_std::smart_table::{Self, SmartTable};

let table = smart_table::new<address, u64>();
smart_table::add(&mut table, @0x1, 100);
let value = smart_table::borrow(&table, @0x1);

aptos_std::smart_vector

A vector that automatically splits into a table when it grows beyond a threshold.

use aptos_std::smart_vector::{Self, SmartVector};

let sv = smart_vector::new<u64>();
smart_vector::push_back(&mut sv, 42);

aptos_std::simple_map

A simple key-value map backed by a vector. Best for small collections.

use aptos_std::simple_map::{Self, SimpleMap};

let map = simple_map::new<String, u64>();
simple_map::add(&mut map, key, value);

aptos_std::table

A hash-based key-value store where each entry occupies its own storage slot.

use aptos_std::table::{Self, Table};

let t = table::new<address, u64>();
table::add(&mut t, @0x1, 100);

Cryptography

aptos_std::ed25519

Ed25519 signature verification.

aptos_std::multi_ed25519

Multi-signature Ed25519 verification.

aptos_std::secp256k1

Secp256k1 elliptic curve operations (compatible with Ethereum keys).

Utilities

aptos_std::type_info

Get runtime type information for generic types.

use aptos_std::type_info;

fun get_type_name<T>(): String {
    type_info::type_name<T>()
}

aptos_std::math64 / aptos_std::math128

Common math operations like min, max, pow, and sqrt.

use aptos_std::math64;

let max_val = math64::max(10, 20); // 20
let min_val = math64::min(10, 20); // 10

aptos_std::comparator

Generic comparison utilities for ordering values.

aptos_std::string_utils

Extended string formatting and manipulation.

use aptos_std::string_utils;

let formatted = string_utils::to_string(&42u64);

Summary

ModulePurpose
smart_tableScalable hash table
smart_vectorAuto-scaling vector
simple_mapSmall key-value map
tableLarge key-value store
ed25519Ed25519 signatures
type_infoRuntime type info
math64/128Math utilities
string_utilsString formatting
comparatorValue comparison

Aptos Framework

The Aptos Framework (aptos_framework) provides the core functionality of the Aptos blockchain, including accounts, coins, objects, governance, and staking. It is deployed at address 0x1 and depends on both MoveStdlib and AptosStdlib.

Source: aptos-framework/aptos-framework

graph
  A[MoveStdLib] --> B[AptosStdLib]
    B --> C[AptosFramework]
    C --> D[AptosToken]
    C --> E[AptosTokenObjects]

Key Modules

Account Management

aptos_framework::account

Manages account creation, authentication key rotation, and signer capabilities.

use aptos_framework::account;

// In tests
#[test_only]
fun setup(account: &signer) {
    account::create_account_for_test(signer::address_of(account));
}

Token Standards

aptos_framework::coin

The legacy fungible token standard (see Coin (Legacy)).

aptos_framework::fungible_asset

The modern fungible token standard (see Fungible Asset).

aptos_framework::primary_fungible_store

Primary stores for fungible assets, automatically created per account.

Object Framework

aptos_framework::object

The object model for creating and managing on-chain objects (see Object Model).

use aptos_framework::object::{Self, Object, ConstructorRef};

fun create_my_object(creator: &signer): ConstructorRef {
    object::create_object(signer::address_of(creator))
}

Events

aptos_framework::event

Event emission for indexing and observability.

use aptos_framework::event;

#[event]
struct MyEvent has drop, store {
    value: u64,
}

fun emit_event() {
    event::emit(MyEvent { value: 42 });
}

Randomness

aptos_framework::randomness

On-chain randomness for games, lotteries, and fair selection.

use aptos_framework::randomness;

#[randomness]
entry fun random_action(account: &signer) {
    let random_number = randomness::u64_integer();
}

Governance and Staking

aptos_framework::aptos_governance

On-chain governance for protocol upgrades and parameter changes.

aptos_framework::staking

Validator staking and delegation.

Utilities

aptos_framework::timestamp

Access the current blockchain timestamp.

use aptos_framework::timestamp;

fun get_current_time(): u64 {
    timestamp::now_seconds()
}

aptos_framework::aptos_coin

The native APT token module.

Summary

ModulePurpose
accountAccount management
coinLegacy fungible tokens
fungible_assetModern fungible tokens
objectObject model
eventEvent emission
randomnessOn-chain randomness
timestampBlockchain time
aptos_coinNative APT token
aptos_governanceOn-chain governance
stakingValidator staking

Building on Aptos with LLMs

Large Language Models (LLMs) are transforming how developers build, audit, and interact with blockchain applications. This chapter covers how to effectively use LLMs as development tools when building on Aptos, and how to build AI-powered applications that integrate with the Aptos blockchain.

Why LLMs and Aptos?

The Aptos ecosystem is uniquely well-suited for LLM-assisted development:

  1. Move is a well-structured language: Move's strict type system, explicit resource management, and formal specification support make it easier for LLMs to generate correct code.
  2. Safety properties are compiler-enforced: Even if an LLM generates imperfect code, the Move compiler catches many classes of bugs.
  3. The llms.txt standard: This book and the Aptos developer documentation are published in the llms.txt format, making them directly consumable by LLMs as context.
  4. Growing AI agent ecosystem: Aptos supports on-chain AI agents that can autonomously manage assets, execute trades, and interact with DeFi protocols.

What You'll Learn

  1. Using LLMs to Write Move Code - Prompt engineering and best practices for generating smart contracts
  2. Using LLMs to Audit Smart Contracts - Leveraging AI for security review
  3. Building AI Agents on Aptos - Creating autonomous on-chain agents
  4. The llms.txt Standard and Aptos - How Aptos documentation is optimized for LLM consumption

The AI-Blockchain Convergence

The intersection of AI and blockchain creates powerful new possibilities:

  • AI agents that manage on-chain portfolios and execute DeFi strategies
  • LLM-powered developer tools that generate, test, and audit Move code
  • Natural language interfaces to blockchain operations
  • Automated smart contract verification using AI-assisted formal methods

Aptos is at the forefront of this convergence, with active development in AI agent frameworks, LLM-optimized documentation, and on-chain infrastructure for autonomous agents.

Using LLMs to Write Move Code

LLMs can significantly accelerate Move development when used effectively. This section covers best practices for prompting LLMs to generate correct, secure, and idiomatic Move code.

Providing Context

The most important factor for high-quality Move code generation is giving the LLM sufficient context about the Aptos ecosystem and Move language.

Using llms.txt

The Aptos Book and Aptos developer documentation are published in the llms.txt format. You can provide this content directly to an LLM:

# In your prompt or system message, include:
# - The relevant chapter of the Aptos Book (e.g., the structs or events chapter)
# - The Aptos Framework documentation for modules you plan to use
# - Your existing code for context

Structuring Your Prompts

A well-structured prompt for Move code generation includes:

  1. Goal: What the contract should do
  2. Context: Relevant Aptos/Move concepts
  3. Constraints: Security requirements, gas considerations
  4. Examples: Similar code patterns from the Aptos ecosystem

Example Prompt

Write a Move module for a simple escrow contract on Aptos.

Requirements:
- Seller deposits an item (represented as a u64 token ID)
- Buyer deposits APT as payment
- Either party can cancel before both sides deposit
- When both sides deposit, the swap executes automatically
- Use the Aptos object model for the escrow
- Emit events for all state changes
- Include comprehensive error codes
- Follow the naming conventions from the Aptos Book

Use these Aptos framework modules:
- aptos_framework::object for the escrow object
- aptos_framework::event for events
- aptos_framework::fungible_asset for APT transfers

Effective Prompt Patterns

Pattern 1: Incremental Development

Build contracts step by step:

Step 1: "Define the struct and error codes for a voting contract"
Step 2: "Add the create_proposal entry function"
Step 3: "Add the vote entry function with duplicate vote prevention"
Step 4: "Add view functions for proposal status"
Step 5: "Add unit tests for the happy path"
Step 6: "Add unit tests for error cases"

Pattern 2: Code Review

Ask the LLM to review generated code:

Review this Move module for:
1. Security vulnerabilities
2. Missing error handling
3. Gas optimization opportunities
4. Incorrect use of abilities
5. Missing events

[paste your code]

Pattern 3: Test Generation

LLMs excel at generating comprehensive tests:

Write unit tests for this Move module. Include:
- Happy path tests for each entry function
- Error case tests with #[expected_failure]
- Edge case tests (zero values, max values, empty collections)
- Multi-user interaction tests

[paste your module]

Pattern 4: Explain and Refactor

Use LLMs to understand existing code:

Explain what this Move module does, including:
- The purpose of each struct and its abilities
- What each function does and when it should be called
- The security model (who can call what)
- Any potential issues

[paste existing code]

Common Pitfalls

1. Outdated Syntax

LLMs may generate Move code using outdated syntax. Key things to watch for:

  • Resource indexing: Modern Move uses Resource[addr] instead of borrow_global<Resource>(addr)
  • Vector methods: Modern Move supports v.push_back(x) instead of vector::push_back(&mut v, x)
  • Event emission: Use #[event] with event::emit() instead of legacy event handles

2. Missing Abilities

LLMs sometimes forget or misapply abilities. Always check:

  • Does the struct need key for global storage?
  • Does the struct need store to be embedded in other stored types?
  • Should the struct lack copy to prevent duplication (for assets)?
  • Should the struct lack drop to prevent accidental loss (for receipts)?

3. Incorrect Error Handling

LLMs may use generic error codes or forget error conditions. Ensure:

  • Every abort has a descriptive error constant
  • Error constants follow the E_ naming convention
  • Doc comments describe the error condition
  • All preconditions are checked

4. Security Assumptions

LLMs may make incorrect security assumptions. Always verify:

  • Signer authorization is required for state changes
  • View functions don't modify state
  • Entry function parameters are validated
  • Access control is enforced

Workflow: LLM-Assisted Development

A recommended workflow for using LLMs in Move development:

  1. Spec: Describe what you want to build in natural language
  2. Generate: Use an LLM to generate the initial code
  3. Review: Manually review the generated code for correctness
  4. Compile: Run aptos move compile to check for compilation errors
  5. Test: Generate and run tests with aptos move test
  6. Audit: Use the LLM to review the code for security issues
  7. Iterate: Refine based on test results and review feedback

LLM Tool Integration

Several development tools integrate LLMs with Move development:

  • Cursor / VS Code with AI: Use the Aptos Move Analyzer extension alongside an AI assistant
  • GitHub Copilot: Provides inline Move code suggestions (ensure you have Aptos context loaded)
  • Claude / GPT with documentation: Feed llms.txt content for context-aware code generation

Best Practices Summary

  1. Always provide Aptos-specific context: Don't assume the LLM knows the latest Move syntax.
  2. Review all generated code: LLMs are assistants, not replacements for developer judgment.
  3. Compile and test: Always verify generated code compiles and passes tests.
  4. Use incremental prompts: Build complex contracts step by step.
  5. Cross-reference with documentation: Verify LLM suggestions against official Aptos docs.
  6. Keep the LLM updated: Provide the latest llms.txt content for current API information.

Using LLMs to Audit Smart Contracts

LLMs can serve as a valuable first line of defense in smart contract security review. While they don't replace professional auditors, they can catch common vulnerabilities and improve code quality before a formal audit.

The AI Audit Workflow

Step 1: Initial Scan

Feed your entire contract to the LLM with a structured audit prompt:

Perform a security audit of this Move smart contract. Check for:

1. Access control issues (missing signer checks, unauthorized state changes)
2. Resource safety (potential for resource duplication or loss)
3. Integer overflow/underflow
4. Reentrancy or cross-module interaction issues
5. Missing input validation
6. Incorrect use of abilities
7. Missing or incorrect error codes
8. Event emission completeness
9. Gas optimization opportunities
10. Upgrade safety (backward compatibility)

For each issue found, provide:
- Severity (Critical / High / Medium / Low / Informational)
- Location (function name and line)
- Description of the issue
- Recommended fix

[paste your contract]

Step 2: Focused Analysis

Drill into specific areas:

Analyze the access control model of this contract:
- Who can call each entry function?
- What state can each caller modify?
- Are there any privilege escalation paths?
- Is the admin role properly protected?

[paste your contract]

Step 3: Test Gap Analysis

Identify missing test coverage:

Given this contract and its test suite, identify:
- Which functions lack test coverage?
- Which error paths are not tested?
- Which edge cases are missing?
- What interaction patterns between functions should be tested?

Contract:
[paste contract]

Tests:
[paste tests]

Common Vulnerabilities LLMs Can Detect

Missing Signer Authorization

// VULNERABLE: No signer check -- anyone can call this
public entry fun set_admin(new_admin: address) acquires Config {
    let config = &mut Config[@my_addr];
    config.admin = new_admin;
}

// FIXED: Requires signer authorization
public entry fun set_admin(current_admin: &signer, new_admin: address) acquires Config {
    let config = &mut Config[@my_addr];
    assert!(signer::address_of(current_admin) == config.admin, E_NOT_ADMIN);
    config.admin = new_admin;
}

Unchecked Arithmetic

// VULNERABLE: Could overflow
public fun add_balance(account: &mut Account, amount: u64) {
    account.balance = account.balance + amount;
}

// FIXED: Check for overflow
public fun add_balance(account: &mut Account, amount: u64) {
    let new_balance = account.balance + amount;
    assert!(new_balance >= account.balance, E_OVERFLOW);
    account.balance = new_balance;
}

Missing Existence Checks

// VULNERABLE: Will abort with an unhelpful error if resource doesn't exist
public fun get_balance(addr: address): u64 acquires Balance {
    Balance[addr].amount
}

// FIXED: Explicit existence check with descriptive error
public fun get_balance(addr: address): u64 acquires Balance {
    assert!(exists<Balance>(addr), E_NO_BALANCE);
    Balance[addr].amount
}

Limitations of LLM Auditing

LLMs have important limitations as auditors:

  1. No execution: LLMs reason about code statically -- they cannot run it.
  2. Context window: Very large contracts may exceed the LLM's context window.
  3. Novel vulnerabilities: LLMs may miss novel attack vectors not present in training data.
  4. False positives: LLMs may flag correct code as vulnerable.
  5. No formal proofs: LLMs provide heuristic analysis, not mathematical proofs.

Combining AI and Human Auditing

The most effective approach combines AI and human review:

StageToolPurpose
DevelopmentLLM-assisted codingGenerate initial code with best practices
Self-reviewLLM auditCatch obvious issues before submission
Compileraptos move compileType checking and ability verification
Testingaptos move testRuntime correctness verification
Formal verificationaptos move proveMathematical guarantees for critical properties
Professional auditHuman auditorsDeep analysis of business logic and novel attacks

Best Practices

  1. Use LLMs as a first pass: Run AI audits before spending on professional auditors.
  2. Don't rely solely on AI: Always have human review for production contracts.
  3. Provide full context: Give the LLM all related modules, not just the one being audited.
  4. Iterate on findings: Fix issues the LLM identifies, then re-audit.
  5. Document assumptions: Ask the LLM to list all assumptions it made during the audit.
  6. Check against known patterns: Ask the LLM to verify your contract follows established design patterns.

Building AI Agents on Aptos

AI agents are autonomous programs that can interact with the blockchain on behalf of users. Aptos provides infrastructure for building AI agents that can manage digital assets, execute DeFi strategies, and participate in governance.

What is an On-Chain AI Agent?

An on-chain AI agent typically consists of:

  1. An off-chain LLM that makes decisions based on on-chain data
  2. A wallet/account that the agent controls for executing transactions
  3. Smart contracts that define what the agent can do and enforce guardrails
  4. An execution loop that reads state, decides on actions, and submits transactions
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│                  │     │                  │     │                 │
│   LLM Engine    │────▶│  Agent Backend   │────▶│  Aptos Chain    │
│  (Decision)     │     │  (Execution)     │     │  (Settlement)   │
│                  │◀────│                  │◀────│                 │
└─────────────────┘     └──────────────────┘     └─────────────────┘

Agent Architecture

1. Read On-Chain State

Use the Aptos SDK to read blockchain state:

import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";

const aptos = new Aptos(new AptosConfig({ network: Network.MAINNET }));

// Read account balance
const balance = await aptos.getAccountAPTAmount({
  accountAddress: agentAddress,
});

// Read contract state via view function
const state = await aptos.view({
  payload: {
    function: "0x1::coin::balance",
    typeArguments: ["0x1::aptos_coin::AptosCoin"],
    functionArguments: [agentAddress],
  },
});

2. LLM Decision Making

Feed the on-chain state to an LLM for decision-making:

const prompt = `
You are an AI agent managing a DeFi portfolio on Aptos.

Current state:
- APT balance: ${balance}
- Current positions: ${JSON.stringify(positions)}
- Market conditions: ${JSON.stringify(marketData)}

Available actions:
1. swap(token_in, token_out, amount) - Swap tokens on a DEX
2. provide_liquidity(pool, amount) - Add liquidity to a pool
3. stake(amount) - Stake APT with a validator
4. hold() - Take no action

Decide the best action based on the current state.
Return your decision as JSON: { "action": "...", "params": {...}, "reasoning": "..." }
`;

const decision = await llm.complete(prompt);

3. Execute Transactions

Submit the decided transaction to the blockchain:

import { Account, Ed25519PrivateKey } from "@aptos-labs/ts-sdk";

// Agent's account (secured in practice)
const agentAccount = Account.fromPrivateKey({
  privateKey: new Ed25519PrivateKey(agentPrivateKey),
});

// Build and submit the transaction
const transaction = await aptos.transaction.build.simple({
  sender: agentAccount.accountAddress,
  data: {
    function: "0x1::coin::transfer",
    typeArguments: ["0x1::aptos_coin::AptosCoin"],
    functionArguments: [recipientAddress, amount],
  },
});

const committedTxn = await aptos.signAndSubmitTransaction({
  signer: agentAccount,
  transaction,
});

await aptos.waitForTransaction({
  transactionHash: committedTxn.hash,
});

Smart Contract Guardrails

For safety, define on-chain guardrails that limit what an agent can do:

module my_addr::agent_vault {
    use std::signer;
    use aptos_framework::timestamp;

    /// Agent is not active
    const E_AGENT_INACTIVE: u64 = 1;
    /// Transfer exceeds per-transaction limit
    const E_TRANSFER_TOO_LARGE: u64 = 2;
    /// Transfer would exceed daily spending limit
    const E_DAILY_LIMIT_EXCEEDED: u64 = 3;

    /// Seconds in a day, used for daily spend reset
    const SECONDS_PER_DAY: u64 = 86400;

    struct AgentConfig has key {
        owner: address,
        daily_spent: u64,
        last_reset_timestamp: u64,
        max_transfer: u64,
        max_daily_spend: u64,
        is_active: bool,
    }

    /// Only the agent owner can configure the agent
    public entry fun configure_agent(
        owner: &signer,
        max_transfer: u64,
        max_daily_spend: u64,
    ) {
        let owner_addr = signer::address_of(owner);
        move_to(owner, AgentConfig {
            owner: owner_addr,
            daily_spent: 0,
            last_reset_timestamp: timestamp::now_seconds(),
            max_transfer,
            max_daily_spend,
            is_active: true,
        });
    }

    /// Reset daily spending if a new day has started
    fun maybe_reset_daily_spent(config: &mut AgentConfig) {
        let now = timestamp::now_seconds();
        if (now - config.last_reset_timestamp >= SECONDS_PER_DAY) {
            config.daily_spent = 0;
            config.last_reset_timestamp = now;
        };
    }

    /// The agent calls this -- guardrails enforce limits
    public entry fun agent_transfer(
        agent: &signer,
        to: address,
        amount: u64,
    ) acquires AgentConfig {
        let agent_addr = signer::address_of(agent);
        let config = &mut AgentConfig[agent_addr];

        assert!(config.is_active, E_AGENT_INACTIVE);
        assert!(amount <= config.max_transfer, E_TRANSFER_TOO_LARGE);

        // Reset daily spending if a new day has started
        maybe_reset_daily_spent(config);
        assert!(config.daily_spent + amount <= config.max_daily_spend, E_DAILY_LIMIT_EXCEEDED);

        config.daily_spent = config.daily_spent + amount;
        // Execute transfer...
    }

    /// Owner can pause the agent at any time
    public entry fun pause_agent(owner: &signer) acquires AgentConfig {
        let config = &mut AgentConfig[signer::address_of(owner)];
        config.is_active = false;
    }
}

Agent Frameworks on Aptos

Several frameworks simplify building AI agents on Aptos:

Move Agent Kit

The Move Agent Kit provides tools for building AI agents that interact with the Aptos blockchain. It integrates with popular LLM frameworks and provides pre-built tools for common on-chain actions.

Key features:

  • Pre-built Aptos transaction tools for LLM agents
  • Integration with LangChain, Eliza, and other agent frameworks
  • Token transfer, NFT minting, and DeFi interaction tools
  • Wallet management and transaction signing

Eliza Framework

The Eliza Framework is a multi-agent simulation framework that supports Aptos blockchain interactions. Agents can:

  • Manage Aptos wallets
  • Execute token transfers
  • Interact with DeFi protocols
  • Monitor on-chain events

Use Cases

Portfolio Manager

An AI agent that:

  • Monitors token prices and portfolio performance
  • Rebalances holdings based on predefined strategies
  • Executes swaps on DEXes for optimal pricing
  • Reports performance to the owner

NFT Trader

An AI agent that:

  • Monitors NFT marketplace listings
  • Evaluates NFT value using image analysis and market data
  • Places bids or buys undervalued NFTs
  • Lists overvalued NFTs for sale

Governance Participant

An AI agent that:

  • Reads governance proposals
  • Analyzes proposal impact using LLM reasoning
  • Votes according to predefined principles
  • Reports voting activity and reasoning

Security Considerations

  1. Key management: Agent private keys must be securely stored (use HSMs or secure enclaves).
  2. Spending limits: Always enforce on-chain guardrails for maximum transaction amounts.
  3. Kill switch: Implement an owner-controlled pause mechanism.
  4. Monitoring: Log all agent actions and set up alerts for unusual behavior.
  5. Testing: Thoroughly test agent behavior on devnet before mainnet deployment.
  6. Prompt injection: Protect against prompt injection attacks if the agent processes external data.

Best Practices

  1. Start small: Begin with simple, low-risk actions and gradually increase agent capabilities.
  2. Use guardrails: Always implement on-chain spending limits and pause mechanisms.
  3. Monitor continuously: Watch agent behavior and be ready to intervene.
  4. Test extensively: Run agents on devnet with simulated market conditions.
  5. Separate concerns: Keep the LLM decision-making separate from transaction execution.
  6. Audit agent contracts: The smart contracts that enforce guardrails are security-critical.

The llms.txt Standard and Aptos

The llms.txt standard provides a way for websites and documentation to be directly consumable by Large Language Models. The Aptos ecosystem embraces this standard to make blockchain development more accessible to AI-assisted workflows.

What is llms.txt?

The llms.txt standard defines a simple, structured text format that:

  • Is easy for LLMs to parse and understand
  • Contains all essential information without HTML, JavaScript, or CSS noise
  • Follows a consistent hierarchy that maps well to LLM context windows
  • Can be directly included in prompts or system messages

How the Aptos Book Uses llms.txt

This book is built with mdbook-llms-txt-tools, which automatically generates llms.txt output alongside the HTML version. The build process creates two LLM-optimized outputs:

llms.txt - Summary Format

A concise overview of the book's structure and key concepts. Useful for giving an LLM a high-level understanding of the Aptos ecosystem.

llms-full.txt - Complete Content

The entire book's content in a flat, LLM-friendly format. Useful for giving an LLM deep knowledge about specific topics.

Using llms.txt in Your Workflow

Providing Context to LLMs

When working with an LLM on Aptos development, include relevant sections of the llms.txt output:

System prompt:
You are an expert Aptos/Move developer. Use the following reference material:

[paste relevant sections from llms-full.txt]

User prompt:
Write a Move module for a token vesting contract...

Building Custom AI Tools

You can fetch the llms.txt file programmatically to build AI-powered tools:

// Fetch the Aptos Book content for LLM context
// This book is hosted at aptos-book.com; the llms-full.txt endpoint
// serves the entire book in a single LLM-friendly text file.
const response = await fetch("https://aptos-book.com/llms-full.txt");
const aptosBookContent = await response.text();

// Include in your LLM prompt
const prompt = `
Reference: ${aptosBookContent}

Task: Generate a Move module for...
`;

IDE Integration

Configure your AI-powered IDE to include Aptos documentation:

  1. Add the llms.txt URL to your editor's AI context sources
  2. Reference specific chapters when asking for code assistance
  3. Use the structured format to provide targeted context

Making Your Own Documentation LLM-Friendly

If you're building on Aptos and want your documentation to be LLM-consumable:

1. Use mdBook with llms-txt-tools

# book.toml
[output.llms-txt]
[output.llms-txt-full]

2. Structure Content Hierarchically

# Main Topic

## Subtopic

### Concept

Explanation with code examples.

```move
// Code example

### 3. Write for Both Humans and LLMs

Good documentation for LLMs:

- **Uses clear headings** that describe the content below
- **Includes complete code examples** that can be used as-is
- **Explains concepts before using them** (define before reference)
- **Uses consistent terminology** throughout
- **Provides context** for code examples (what problem they solve)
- **Lists prerequisites** when building on earlier concepts

### 4. Include Structured Data

Tables, lists, and structured examples help LLMs extract information:

```markdown
| Feature | Description | Example |
|---|---|---|
| `key` | Stored in global storage | `struct Token has key { ... }` |
| `store` | Can be nested | `struct Metadata has store { ... }` |

The Broader Aptos AI Ecosystem

The Aptos ecosystem is actively building infrastructure for AI-blockchain integration:

  • llms.txt support: Documentation optimized for LLM consumption
  • AI agent frameworks: Tools for building autonomous on-chain agents
  • Smart contract templates: LLM-friendly templates for common patterns
  • API documentation: REST and GraphQL APIs documented in LLM-friendly formats

Benefits for Developers

By using llms.txt-optimized documentation:

  1. Faster onboarding: LLMs can provide accurate Aptos-specific guidance
  2. Better code generation: LLMs produce more correct Move code with proper context
  3. Reduced errors: AI assistants catch common mistakes when given comprehensive documentation
  4. Always up-to-date: llms.txt files are generated from the latest documentation
  5. Language-agnostic: LLMs can explain concepts in any natural language, making Aptos accessible globally

Learning Resources

Official Documentation

Tools

Community

AI and LLM Resources

  • llms.txt Standard - The standard for LLM-optimized documentation
  • Move Agent Kit - AI agent toolkit for Aptos
  • This book's llms.txt output - Available at the book's URL with /llms.txt and /llms-full.txt paths

Example Projects

Appendix

This section contains supplementary material to support the main content of the book. Here you'll find helpful resources like detailed installation guides, a glossary of terms, and other useful references.

CLI Installation Methods

This appendix provides detailed instructions for installing and upgrading the Aptos CLI on various operating systems. For a quick start, see the Installation page.

Packaging status


macOS

To install:

brew install aptos

To upgrade:

brew upgrade aptos

Script Installation

Run the following command for both initial installation and upgrading:

curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh

Linux

Script Installation

To install:

curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh

Note on CPU Compatibility If you encounter an Illegal instruction error, your CPU may not support certain SIMD instructions. This can happen on older processors or when running in specific virtualized environments (e.g., Ubuntu x86_64 on an ARM Mac).

Use the following command instead for a generic build:

curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh -s -- --generic-linux

To upgrade:

You can either use the built-in update command:

aptos update aptos

Or, re-run the installation script:

curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh

Windows

To install:

winget install aptos.aptos-cli

To upgrade:

winget upgrade aptos.aptos-cli

Chocolatey

To install (in an administrative shell):

choco install aptos-cli

To upgrade (in an administrative shell):

choco upgrade aptos-cli

Script Installation

To install:

Run the following command in PowerShell. This will set the execution policy for the current user and then run the installer.

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; iwr https://aptos.dev/scripts/install_cli.ps1 | iex

To upgrade:

You can either use the built-in update command:

aptos update aptos

Or, re-run the installation script:

iwr https://aptos.dev/scripts/install_cli.ps1 | iex

Glossary

This glossary defines key terms used throughout the Aptos documentation.


Abort Code

A numeric identifier indicating why a program terminated abnormally. In Move, it signals specific error conditions.

Aptos CLI

The command-line interface for the Aptos blockchain. It is used for compiling, testing, deploying, and interacting with Move packages.

Attributes

Special annotations in Move code, like #[test] or #[view], that provide metadata to the compiler or runtime, modifying the behavior of the annotated code.

BCS (Binary Canonical Serialization)

The deterministic serialization format used in Aptos. BCS ensures that data structures always serialize to the same byte representation, which is crucial for blockchain consensus.

Custom Error Type

A user-defined struct that provides detailed context for an error, offering a more informative alternative to simple abort codes.

Entry Function

A public function in a Move module, marked with the entry keyword, that can be directly invoked in a transaction.

Enum

A type that can hold one of several defined variants. Enums are useful for representing states or options in a type-safe way.

Error Propagation

The process where an error (abort) in one function causes its calling functions to also abort, passing the error up the call stack.

Error Range

A designated range of abort codes for a specific module to prevent conflicts and organize error handling.

Error Testing

The practice of writing tests to verify that functions fail as expected under error conditions, often using the #[expected_failure] attribute.

Event

A mechanism for Move contracts to emit data that can be indexed and queried by off-chain services, signaling that something of interest has occurred.

Expected Failure

A test attribute (#[expected_failure]) that asserts a test case should fail with a specific abort code, enabling robust error-handling tests.

Legacy Shortened Address

A condensed form of an Aptos address where leading zeros are omitted. These are automatically padded with zeros for compatibility.

Little-endian

A byte-ordering scheme where the least significant byte is stored first. Aptos uses little-endian byte order for BCS serialization.

Module

A single file containing Move code (.move), which acts as a fundamental unit of code organization and encapsulation within a package.

Named Address

An alias for a specific account address used in Move source code. The actual address is substituted during compilation, making code more portable.

Package

A deployable unit of Move code that can contain one or more modules. It is published to the Aptos blockchain as a single entity.

Publish

The act of deploying a Move package to the Aptos blockchain, making its code and resources available on-chain.

Resource

A special struct with the key ability that represents data stored in an account's global storage. Resources have ownership semantics and cannot be copied or discarded.

Storage Slot

A location in the blockchain's global state, identified by a 32-byte address, where data (like accounts or objects) can be stored.

Struct

A composite data type that groups related values into a single, named structure. Its behavior is defined by its abilities (copy, drop, store, key).

Tuple

A fixed-size, ordered collection of values that can have different types.

ULEB128 (Unsigned Little-Endian Base-128)

A variable-length encoding for unsigned integers used in BCS, primarily for representing the length of sequences efficiently.

View Function

A read-only function marked with the #[view] attribute. It can be called to query blockchain state without submitting a transaction and incurring gas fees for state changes.

Move Module

/// Test module
///
module module_addr::test_module {

    use std::option::{Self, Option};
    use std::signer;
    use aptos_std::smart_table::{Self, SmartTable};
    use aptos_framework::dispatchable_fungible_asset;
    use aptos_framework::fungible_asset::{Self, Metadata, FungibleStore};
    use aptos_framework::object::{Self, Object, ExtendRef, DeleteRef};
    use aptos_framework::primary_fungible_store;
    use aptos_framework::timestamp;

    /// The lookup to object for escrow in an easily addressable spot
    ///
    /// The main purpose here is to provide fully removable types to allow for full recovery of storage refunds, and not
    /// have a duplicate object.
    struct LockupRef has key {
        lockup_address: address,
    }

    #[resource_group_member(group = aptos_framework::object::ObjectGroup)]
    /// A single lockup, which has the same lockup period for all of them
    ///
    /// These are stored on objects, which map to the appropriate escrows
    enum Lockup has key {
        /// SmartTable implementation, which can be replaced with a newer version later
        ST {
            // Creator of the lockup
            creator: address,
            /// Used to control funds in the escrows
            extend_ref: ExtendRef,
            /// Used to cleanup the Lockup object
            delete_ref: DeleteRef,
            /// Normally with coin, we could escrow in the table, but we have to escrow in owned objects for the purposes of FA
            escrows: SmartTable<EscrowKey, address>
        }
    }

    /// A key used for keeping track of all escrows in an easy to find place
    enum EscrowKey has store, copy, drop {
        FAPerUser {
            /// Marker for which FA is stored
            fa_metadata: Object<Metadata>,
            /// The user in which it's being stored for
            user: address,
        }
    }

    #[resource_group_member(group = aptos_framework::object::ObjectGroup)]
    /// An escrow object for a single user and a single FA
    enum Escrow has key {
        Simple {
            /// The original owner
            original_owner: address,
            /// Used for cleaning up the escrow
            delete_ref: DeleteRef,
        },
        TimeUnlock {
            /// The original owner
            original_owner: address,
            /// Time that the funds can be unlocked
            unlock_secs: u64,
            /// Used for cleaning up the escrow
            delete_ref: DeleteRef,
        }
    }

    // -- Errors --

    /// Lockup already exists at this address
    const E_LOCKUP_ALREADY_EXISTS: u64 = 1;
    /// Lockup not found at address
    const E_LOCKUP_NOT_FOUND: u64 = 2;
    /// No lockup was found for this user and this FA
    const E_NO_USER_LOCKUP: u64 = 3;
    /// Unlock time has not yet passed
    const E_UNLOCK_TIME_NOT_YET: u64 = 4;
    /// Not original owner or lockup owner
    const E_NOT_ORIGINAL_OR_LOCKUP_OWNER: u64 = 5;
    /// Not a time lockup
    const E_NOT_TIME_LOCKUP: u64 = 6;
    /// Not a simple lockup
    const E_NOT_SIMPLE_LOCKUP: u64 = 7;
    /// Can't shorten lockup time
    const E_CANNOT_SHORTEN_LOCKUP_TIME: u64 = 8;

    /// Initializes a lockup at an address
    public entry fun initialize_lockup(
        caller: &signer,
    ) {
        init_lockup(caller);
    }

    inline fun init_lockup(caller: &signer): Object<Lockup> {
        let caller_address = signer::address_of(caller);

        // Create the object only if it doesn't exist, otherwise quit out
        assert!(!exists<LockupRef>(caller_address), E_LOCKUP_ALREADY_EXISTS);

        // Create the object
        let constructor_ref = object::create_object(@0x0);
        let lockup_address = object::address_from_constructor_ref(&constructor_ref);
        let extend_ref = object::generate_extend_ref(&constructor_ref);
        let delete_ref = object::generate_delete_ref(&constructor_ref);
        let obj_signer = object::generate_signer(&constructor_ref);
        move_to(&obj_signer, Lockup::ST {
            creator: caller_address,
            escrows: smart_table::new(),
            extend_ref,
            delete_ref
        });

        // This is specifically to ensure that we don't create two lockup objects, we put a marker in the account
        move_to(caller, LockupRef {
            lockup_address
        });
        object::object_from_constructor_ref(&constructor_ref)
    }

    /// Escrows funds with a user defined lockup time
    public entry fun escrow_funds_with_no_lockup(
        caller: &signer,
        lockup_obj: Object<Lockup>,
        fa_metadata: Object<Metadata>,
        amount: u64,
    ) acquires Lockup, Escrow {
        let caller_address = signer::address_of(caller);
        let lockup_address = object::object_address(&lockup_obj);
        let lockup = &mut Lockup[lockup_address];

        let lockup_key = EscrowKey::FAPerUser {
            fa_metadata,
            user: caller_address
        };

        let escrow_address = lockup.escrows.borrow_mut_with_default(lockup_key, @0x0);

        // If we haven't found it, create a new escrow object
        if (escrow_address == &@0x0) {
            let constructor_ref = object::create_object(lockup_address);
            let object_signer = object::generate_signer(&constructor_ref);
            let object_delete_ref = object::generate_delete_ref(&constructor_ref);

            // Make it a store to keep the escrow funds
            fungible_asset::create_store(&constructor_ref, fa_metadata);

            // Store the appropriate info for the funds
            move_to(&object_signer, Escrow::Simple {
                original_owner: caller_address,
                delete_ref: object_delete_ref
            });
            // Save it to the table
            *escrow_address = object::address_from_constructor_ref(&constructor_ref);
        } else {
            // Otherwise, we'll reset the unlock time to the new time
            let escrow = &Escrow[*escrow_address];
            match (escrow) {
                Simple { .. } => {
                    // Do nothing
                }
                TimeUnlock { .. } => {
                    abort E_NOT_SIMPLE_LOCKUP;
                }
            };
        };

        // Now transfer funds into the escrow
        escrow_funds(caller, fa_metadata, *escrow_address, caller_address, amount);
    }

    /// Escrows funds with a user defined lockup time
    public entry fun escrow_funds_with_time(
        caller: &signer,
        lockup_obj: Object<Lockup>,
        fa_metadata: Object<Metadata>,
        amount: u64,
        lockup_time_secs: u64,
    ) acquires Lockup, Escrow {
        let caller_address = signer::address_of(caller);
        let lockup_address = object::object_address(&lockup_obj);
        let lockup = &mut Lockup[lockup_address];

        let lockup_key = EscrowKey::FAPerUser {
            fa_metadata,
            user: caller_address
        };


        let escrow_address = lockup.escrows.borrow_mut_with_default(lockup_key, @0x0);
        // TODO: Do we make this specified by the contract rather than user?
        let new_unlock_secs = timestamp::now_seconds() + lockup_time_secs;

        // If we haven't found it, create a new escrow object
        if (escrow_address == &@0x0) {
            // We specifically make this object on @0x0, so that the creator doesn't have the ability to pull the funds
            // out without the contract
            let constructor_ref = object::create_object(lockup_address);
            let object_signer = object::generate_signer(&constructor_ref);
            let object_delete_ref = object::generate_delete_ref(&constructor_ref);

            // Make it a store to keep the escrow funds
            fungible_asset::create_store(&constructor_ref, fa_metadata);

            // Store the appropriate info for the funds
            move_to(&object_signer, Escrow::TimeUnlock {
                original_owner: caller_address,
                unlock_secs: new_unlock_secs,
                delete_ref: object_delete_ref
            });
            // Save it to the table
            *escrow_address = object::address_from_constructor_ref(&constructor_ref);
        } else {
            // Otherwise, we'll reset the unlock time to the new time
            let escrow = &mut Escrow[*escrow_address];
            match (escrow) {
                Simple { .. } => {
                    abort E_NOT_TIME_LOCKUP;
                }
                TimeUnlock { unlock_secs, .. } => {
                    // We however, cannot shorten the unlock time
                    if (*unlock_secs > new_unlock_secs) {
                        abort E_CANNOT_SHORTEN_LOCKUP_TIME;
                    } else {
                        *unlock_secs = new_unlock_secs
                    }
                }
            };
        };

        // Now transfer funds into the escrow
        escrow_funds(caller, fa_metadata, *escrow_address, caller_address, amount);
    }

    /// Claims an escrow by the owner of the escrow
    public entry fun claim_escrow(
        caller: &signer,
        lockup_obj: Object<Lockup>,
        fa_metadata: Object<Metadata>,
        user: address,
    ) acquires Lockup, Escrow {
        let caller_address = signer::address_of(caller);
        let lockup = get_lockup_mut(&lockup_obj);
        assert!(caller_address == lockup.creator, E_NOT_ORIGINAL_OR_LOCKUP_OWNER);
        let (lockup_key, escrow_address) = lockup.get_escrow(
            fa_metadata,
            user
        );

        // Take funds from lockup
        lockup.take_funds(fa_metadata, escrow_address);

        // Clean up the object
        lockup.delete_escrow(lockup_key);
    }

    /// Returns funds for the user
    ///
    /// TODO: add additional entry function for using LockupRef
    public entry fun return_user_funds(
        caller: &signer,
        lockup_obj: Object<Lockup>,
        fa_metadata: Object<Metadata>,
        user: address,
    ) acquires Lockup, Escrow {
        let caller_address = signer::address_of(caller);
        let lockup = get_lockup_mut(&lockup_obj);
        assert!(caller_address == lockup.creator, E_NOT_ORIGINAL_OR_LOCKUP_OWNER);
        let (lockup_key, escrow_address) = lockup.get_escrow(
            fa_metadata,
            user
        );

        // Determine original owner, and any conditions on returning
        let original_owner = match (&Escrow[escrow_address]) {
            Escrow::Simple { original_owner, .. } => {
                *original_owner
            }
            Escrow::TimeUnlock { original_owner, .. } => {
                // Note, the lockup owner can reject the unlock faster than the unlock time
                *original_owner
            }
        };

        lockup.return_funds(fa_metadata, escrow_address, original_owner);

        // Clean up the object
        lockup.delete_escrow(lockup_key);
    }

    /// Returns funds for the caller
    ///
    /// TODO: add additional entry function for using LockupRef
    public entry fun return_my_funds(
        caller: &signer,
        lockup_obj: Object<Lockup>,
        fa_metadata: Object<Metadata>,
    ) acquires Lockup, Escrow {
        let caller_address = signer::address_of(caller);
        let lockup = get_lockup_mut(&lockup_obj);
        let (lockup_key, escrow_address) = lockup.get_escrow(
            fa_metadata,
            caller_address
        );

        // Determine original owner, and any conditions on returning
        let original_owner = match (&Escrow[escrow_address]) {
            Escrow::Simple { original_owner, .. } => {
                *original_owner
            }
            Escrow::TimeUnlock { original_owner, unlock_secs, .. } => {
                assert!(timestamp::now_seconds() >= *unlock_secs, E_UNLOCK_TIME_NOT_YET);
                *original_owner
            }
        };

        // To prevent others from being annoying, only the original owner can return funds
        assert!(original_owner == caller_address, E_NOT_ORIGINAL_OR_LOCKUP_OWNER);
        lockup.return_funds(fa_metadata, escrow_address, original_owner);

        // Clean up the object
        lockup.delete_escrow(lockup_key);
    }

    /// Retrieves the lockup object for mutation
    inline fun get_lockup_mut(
        lockup_obj: &Object<Lockup>,
    ): &mut Lockup {
        let lockup_address = object::object_address(lockup_obj);
        &mut Lockup[lockup_address]
    }

    /// Retrieves the lockup object for reading
    inline fun get_lockup(
        lockup_obj: &Object<Lockup>,
    ): &Lockup {
        let lockup_address = object::object_address(lockup_obj);
        &Lockup[lockup_address]
    }

    /// Retrieves the lockup object for removal
    inline fun get_escrow(
        self: &mut Lockup,
        fa_metadata: Object<Metadata>,
        user: address
    ): (EscrowKey, address) {
        let lockup_key = EscrowKey::FAPerUser {
            fa_metadata,
            user,
        };

        assert!(self.escrows.contains(lockup_key), E_NO_USER_LOCKUP);

        (lockup_key, *self.escrows.borrow(lockup_key))
    }

    /// Escrows an amount of funds to the escrow object
    inline fun escrow_funds(
        caller: &signer,
        fa_metadata: Object<Metadata>,
        escrow_address: address,
        caller_address: address,
        amount: u64
    ) {
        let store_obj = object::address_to_object<FungibleStore>(escrow_address);
        let caller_primary_store = primary_fungible_store::primary_store_inlined(caller_address, fa_metadata);
        dispatchable_fungible_asset::transfer(caller, caller_primary_store, store_obj, amount);
    }

    /// Returns all outstanding funds
    inline fun take_funds(
        self: &Lockup,
        fa_metadata: Object<Metadata>,
        escrow_address: address,
    ) {
        // Transfer funds back to the original owner
        let escrow_object = object::address_to_object<FungibleStore>(escrow_address);
        let balance = fungible_asset::balance(escrow_object);
        let primary_store = primary_fungible_store::ensure_primary_store_exists(self.creator, fa_metadata);

        // Use dispatchable because we don't know if it uses it
        let lockup_signer = object::generate_signer_for_extending(&self.extend_ref);
        dispatchable_fungible_asset::transfer(&lockup_signer, escrow_object, primary_store, balance);
    }

    /// Returns all outstanding funds
    inline fun return_funds(
        self: &Lockup,
        fa_metadata: Object<Metadata>,
        escrow_address: address,
        original_owner: address
    ) {
        // Transfer funds back to the original owner
        let escrow_object = object::address_to_object<FungibleStore>(escrow_address);
        let balance = fungible_asset::balance(escrow_object);
        let original_owner_primary_store = primary_fungible_store::primary_store_inlined(
            original_owner,
            fa_metadata
        );
        // Use dispatchable because we don't know if it uses it
        let lockup_signer = object::generate_signer_for_extending(&self.extend_ref);
        dispatchable_fungible_asset::transfer(&lockup_signer, escrow_object, original_owner_primary_store, balance);
    }

    /// Deletes an escrow object
    inline fun delete_escrow(self: &mut Lockup, lockup_key: EscrowKey) {
        let escrow_addr = self.escrows.remove(lockup_key);

        // The following lines will return the storage deposit
        let delete_ref = match (move_from<Escrow>(escrow_addr)) {
            Escrow::Simple { delete_ref, .. } => {
                delete_ref
            }
            Escrow::TimeUnlock { delete_ref, .. } => {
                delete_ref
            }
        };
        fungible_asset::remove_store(&delete_ref);
        object::delete(delete_ref);
    }

    #[view]
    /// Tells the lockup address for the user who created the original lockup
    public fun lockup_address(escrow_account: address): address acquires LockupRef {
        LockupRef[escrow_account].lockup_address
    }

    #[view]
    /// Tells the amount of escrowed funds currently available
    public fun escrowed_funds(
        lockup_obj: Object<Lockup>,
        fa_metadata: Object<Metadata>,
        user: address
    ): Option<u64> acquires Lockup {
        let lockup = get_lockup(&lockup_obj);
        let escrow_key = EscrowKey::FAPerUser {
            fa_metadata,
            user
        };
        if (lockup.escrows.contains(escrow_key)) {
            let escrow_address = lockup.escrows.borrow(escrow_key);
            let escrow_obj = object::address_to_object<Escrow>(*escrow_address);
            option::some(fungible_asset::balance(escrow_obj))
        } else {
            option::none()
        }
    }

    #[view]
    /// Tells the amount of escrowed funds currently available
    public fun remaining_escrow_time(
        lockup_obj: Object<Lockup>,
        fa_metadata: Object<Metadata>,
        user: address
    ): Option<u64> acquires Lockup, Escrow {
        let lockup = get_lockup(&lockup_obj);
        let escrow_key = EscrowKey::FAPerUser {
            fa_metadata,
            user
        };
        if (lockup.escrows.contains(escrow_key)) {
            let escrow_address = lockup.escrows.borrow(escrow_key);
            let remaining_secs = match (&Escrow[*escrow_address]) {
                Simple { .. } => { 0 }
                TimeUnlock { unlock_secs, .. } => {
                    let now = timestamp::now_seconds();
                    if (now >= *unlock_secs) {
                        0
                    } else {
                        *unlock_secs - now
                    }
                }
            };
            option::some(remaining_secs)
        } else {
            option::none()
        }
    }

    #[test_only]
    const TWO_HOURS_SECS: u64 = 2 * 60 * 60;

    #[test_only]
    fun setup_for_test(
        framework: &signer,
        asset: &signer,
        creator: &signer,
        user: &signer
    ): (address, address, Object<Metadata>, Object<Lockup>) {
        timestamp::set_time_has_started_for_testing(framework);
        let (creator_ref, metadata) = fungible_asset::create_test_token(asset);
        let (mint_ref, _transfer_ref, _burn_ref) = primary_fungible_store::init_test_metadata_with_primary_store_enabled(
            &creator_ref
        );
        let creator_address = signer::address_of(creator);
        let user_address = signer::address_of(user);
        primary_fungible_store::mint(&mint_ref, user_address, 100);
        let fa_metadata: Object<Metadata> = object::convert(metadata);
        let lockup_obj = init_lockup(creator);
        (creator_address, user_address, fa_metadata, lockup_obj)
    }

    #[test(framework = @0x1, asset = @0xAAAAA, creator = @0x10C0, user = @0xCAFE)]
    fun test_out_flow(framework: &signer, asset: &signer, creator: &signer, user: &signer) acquires Lockup, Escrow {
        let (creator_address, user_address, fa_metadata, lockup_obj) = setup_for_test(framework, asset, creator, user);

        escrow_funds_with_no_lockup(user, lockup_obj, fa_metadata, 5);

        assert!(primary_fungible_store::balance(user_address, fa_metadata) == 95);

        // Check view functions
        assert!(remaining_escrow_time(lockup_obj, fa_metadata, user_address) == option::some(0));
        assert!(escrowed_funds(lockup_obj, fa_metadata, user_address) == option::some(5));
        assert!(remaining_escrow_time(lockup_obj, fa_metadata, @0x1234567) == option::none());
        assert!(escrowed_funds(lockup_obj, fa_metadata, @0x1234567) == option::none());

        // Should be able to return funds immediately
        return_user_funds(creator, lockup_obj, fa_metadata, user_address);
        assert!(primary_fungible_store::balance(user_address, fa_metadata) == 100);

        // Same with the user
        escrow_funds_with_no_lockup(user, lockup_obj, fa_metadata, 5);
        return_my_funds(user, lockup_obj, fa_metadata);
        assert!(primary_fungible_store::balance(user_address, fa_metadata) == 100);

        // Claim an escrow
        escrow_funds_with_no_lockup(user, lockup_obj, fa_metadata, 5);
        claim_escrow(creator, lockup_obj, fa_metadata, user_address);
        assert!(primary_fungible_store::balance(user_address, fa_metadata) == 95);
        assert!(primary_fungible_store::balance(creator_address, fa_metadata) == 5);

        // -- Now test with time lockup --

        escrow_funds_with_time(user, lockup_obj, fa_metadata, 5, TWO_HOURS_SECS);
        assert!(primary_fungible_store::balance(user_address, fa_metadata) == 90);

        // Check view functions
        assert!(remaining_escrow_time(lockup_obj, fa_metadata, user_address) == option::some(TWO_HOURS_SECS));
        assert!(escrowed_funds(lockup_obj, fa_metadata, user_address) == option::some(5));

        // Should be able to return funds immediately
        return_user_funds(creator, lockup_obj, fa_metadata, user_address);
        assert!(primary_fungible_store::balance(user_address, fa_metadata) == 95);

        escrow_funds_with_time(user, lockup_obj, fa_metadata, 5, TWO_HOURS_SECS);

        // User can't unescrow without time passing, let's go forward 2 hours
        timestamp::fast_forward_seconds(TWO_HOURS_SECS);
        return_my_funds(user, lockup_obj, fa_metadata);
        assert!(primary_fungible_store::balance(user_address, fa_metadata) == 95);

        // Claim an escrow, can be immediate
        escrow_funds_with_time(user, lockup_obj, fa_metadata, 5, TWO_HOURS_SECS);
        claim_escrow(creator, lockup_obj, fa_metadata, user_address);
        assert!(primary_fungible_store::balance(user_address, fa_metadata) == 90);
        assert!(primary_fungible_store::balance(creator_address, fa_metadata) == 10);
    }

    #[test(framework = @0x1, asset = @0xAAAAA, creator = @0x10C0, user = @0xCAFE)]
    #[expected_failure(abort_code = E_UNLOCK_TIME_NOT_YET, location = lockup_deployer::fa_lockup)]
    fun test_too_short_lockup(
        framework: &signer,
        asset: &signer,
        creator: &signer,
        user: &signer
    ) acquires Lockup, Escrow {
        let (_creator_address, _user_address, fa_metadata, lockup_obj) = setup_for_test(
            framework,
            asset,
            creator,
            user
        );
        escrow_funds_with_time(user, lockup_obj, fa_metadata, 5, TWO_HOURS_SECS);

        // User can't return funds without waiting for lockup
        return_my_funds(user, lockup_obj, fa_metadata);
    }

    #[test_only]
    public fun test_only_addr() {
        @module_addr
    }
}

Comments

/**
 Block doc comments
*/
module 0x1234::test_module_no_addr {
    const HEX: vector<u8> = x"0123456789ABCDEFabcdef";
    const BYTE: vector<u8> = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+=<>,./?':;\"`~!@#$%^&*()    ";

    /*
      Block comments
    */

    // One line comment

    /// Doc comment
    fun do_nothing(): bool { true }
}

Script

script {
    use aptos_framework::object::Object;
    use aptos_framework::option::Option;
    use aptos_framework::aptos_account;
    use aptos_std::signer;
    use aptos_framework::object;

    /// Comment describing function
    fun some_Name<T: key, U: drop>(
        account1: &signer,
        account2: &signer,
        i1: u8,
        i2: u16,
        i3: u32,
        i4: u64,
        i5: u128,
        i6: u256,
        i7: address,
        i8: bool,
        i9: vector<u8>,
        i10: Object<T>,
        i11: Option<u256>,
        _I_12: U
    ) {
        // TODO: add more info here
        let total = (i1 as u256) + (i2 as u256) + (i3 as u256) + (i4 as u256) + (i5 as u256) + i6;

        if (i8) total *= 2 else total /= 2;
        if (i9.length() >= 1) {
            total += (i9[0] as u256);
        } else if (i11.is_some()) {
            total -= i11.destroy_some();
        } else {
            total %= 2
        };
        let receiver = signer::address_of(account2);
        let balance: u64 = (total / 3) as u64;
        aptos_account::transfer(account1, receiver, balance);
        aptos_account::transfer(account1, i7, balance);
        let addr = object::object_address(&i10);
        aptos_account::transfer(account1, addr, balance);
    }
}

Separate address

// Legacy syntax
address 0x1234 {
/**
 Block doc comments
*/
module test_module_no_addr {
    const HEX: vector<u8> = x"0123456789ABCDEFabcdef";
    const BYTE: vector<u8> = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+=<>,./?':;\"`~!@#$%^&*()    ";

    /*
      Block comments
    */

    // One line comment

    /// Doc comment
    fun do_nothing(): bool { true }
}
}