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.

This book will guide you through everything you need to know, from setting up your development environment and learning the fundamentals of Move to mastering advanced topics and design patterns.

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.

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. You can always find a fairly current list of many editors and IDEs.

TODO: add links to the IDEs and editors that support Move.

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! TODO: add link to visit it in the explorer.

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::hello_blockchain::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::hello_blockchain::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

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.

TODO: Code example updating the tests

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

Struct Basics

Structs and How They Become Resources

Structs and Abilities

Example Program Using Structs

Struct Method Syntax

Enums and Pattern Matching

Defining Enums

Pattern Matching with Enums

Managing Modules and Packages

Module Basics

Module Imports

Function Scopes

Package Basics

Package Dependencies

Package Publishing

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

Writing Unit Tests

Running Unit Tests

Coverage

Formal Verification

Closures, Lambdas, and Function Values

Some of the extensions that Aptos has added to the Move language are the ability to add closures.

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

TODO: No code indexing

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.

TODO: GIF of block STM

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

Account Authorization by Signer

Event Emission

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: TODO: Add LaTeX to make this nicer

(gas used x gas unit price) + storage refund = total gas

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

These are from third parties, and can be used as well.

TODO

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 Stdlib

TODO: Link source, possibly give a high level overview

Aptos Stdlib

TODO: Link source, possibly give a high level overview

Aptos Framework

TODO: Link source, possibly give a high level overview

graph
  A[MoveStdLib] --> B[AptosStdLib]
    B --> C[AptosFramework]
    C --> D[AptosToken]
    C --> E[AptosTokenObjects]

Learning Resources

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 }
}
}