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?
- New to Aptos? Begin with the Introduction to learn more about the Aptos blockchain.
- Ready to code? Jump right into Getting 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:
- Identify the section being updated
- Provide a concise description of the change
- Note any gaps or missing areas
Getting Started
Let's get you started with the Aptos Blockchain! We'll discuss:
- Installing the Aptos CLI on macOS, Linux, and Windows.
- Hello Blockchain! - a simple contract that writes and reads data on-chain.
- 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:
- Build and publish a contract
- Write data on-chain
- 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 addresshello_blockchain
in theMove.toml
file to thetutorial
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
anddrop
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 thepublic
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 onesigner
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 account0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a
, 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:
- Pass in a number to guess
- The same account will guess until they get the same number
- When it's guessed, the game is over and can be reset.
- 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 signer
s 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 directly5
.
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:
- Variables: Named storage locations that hold values
- Data Types: Classifications that define the nature and operations of values
- Functions: Reusable blocks of code that perform specific tasks
- Comments: Documentation that explains code behavior
- 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:
- Variables: Start with basic value storage and manipulation
- Data Types: Understand the type system and available types
- Functions: Learn to organize code into reusable units
- Comments: Document code for maintainability
- Control Flow: Control program execution flow
Best Practices
Principle 1.1 (Move Best Practices) When writing Move code:
- Use Resources for Assets: Represent digital assets as resources
- Minimize Abilities: Only add abilities when necessary
- Explicit Ownership: Make ownership transfers explicit
- Type Safety: Leverage the type system for correctness
- Documentation: Comment complex logic and invariants
Common Pitfalls
Warning 1.1 (Common Mistakes) Avoid these common mistakes in Move:
- Forgetting Abilities: Not adding required abilities to types
- Resource Leaks: Creating resources without proper cleanup
- Type Mismatches: Using wrong types in operations
- Unsafe References: Creating references that could become invalid
- 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
- Exercise 1.1: Create a simple resource type with appropriate abilities
- Exercise 1.2: Identify the abilities required for different use cases
- Exercise 1.3: Analyze the safety properties of a given Move program
- Exercise 1.4: Compare Move's type system with other programming languages
References
- Move Language Documentation
- Aptos Developer Resources
- Move Prover User Guide
- 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
- Start with private: Make functions private by default
- Use public sparingly: Only expose what's necessary
- Use friend for related modules: When modules need to share functionality
- Use package for internal APIs: For functions used within the package
Entry Functions
- Validate input: Always validate parameters in entry functions
- Use clear names: Make function names descriptive
- Handle errors gracefully: Use proper error handling
- Keep them simple: Delegate complex logic to private functions
View Functions
- Mark with
#[view]
: Always mark read-only functions with#[view]
- Don't modify state: View functions should be pure
- Handle missing data: Return sensible defaults for missing data
- Optimize for queries: Make view functions efficient
Test Functions
- Use
#[test_only]
: Mark test functions appropriately - Test edge cases: Include boundary condition tests
- Use descriptive names: Make test names clear
- Test error conditions: Include tests for error cases
Inline Functions
- Keep them small: Inline functions should be simple
- Use for hot paths: Inline frequently called functions
- Don't overuse: Let the compiler decide when to inline
- 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 (///
)
- Use for public APIs: Always document public functions, structs, and modules
- Be descriptive: Explain what the code does, not how it does it
- Include examples: When helpful, include usage examples
- Document parameters and return values: Explain what each parameter does and what the function returns
- 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 (//
)
- Explain complex logic: Use comments to explain non-obvious code
- Avoid obvious comments: Don't comment on what the code obviously does
- Use TODO/FIXME: Mark areas that need attention
- 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 (/* */
)
- Use for block comments: When you need to comment out large sections
- Document complex algorithms: Explain multi-step processes
- 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
- Public APIs: Always document public functions, structs, and modules
- Complex logic: Explain non-obvious algorithms or business logic
- Assumptions: Document any assumptions your code makes
- Limitations: Note any limitations or edge cases
- Dependencies: Explain dependencies on external modules or systems
What Not to Comment
- Obvious code: Don't comment on what the code obviously does
- Outdated information: Don't leave comments that are no longer accurate
- Implementation details: Focus on what, not how (unless the how is complex)
Comment Style
- Be concise: Keep comments brief but informative
- Use proper grammar: Write comments in clear, grammatically correct language
- Be consistent: Use consistent terminology and style throughout your codebase
- 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
- Use early returns: Return early to reduce nesting
- Keep conditions simple: Break complex conditions into multiple if statements
- 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
- Ensure termination: Make sure loops will eventually terminate
- Use break for early exit: Use break instead of complex conditions
- 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
- Use for loops for known ranges: Prefer for loops when you know the iteration count
- Avoid modifying loop variables: Don't modify the loop variable inside the loop
- 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
- Handle all cases: Ensure all enum variants are covered
- Use meaningful patterns: Use descriptive variable names in patterns
- 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:
- Memory Safety: No dangling references, use-after-free, or double-free errors
- Thread Safety: Values cannot be accessed from multiple threads simultaneously
- Resource Management: Resources are explicitly managed and cannot be lost
- 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:
- Single Owner: Each value has exactly one owner
- Move Semantics: When a value is assigned or passed to a function, ownership is transferred
- 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:
- Immutable References: Multiple immutable references can exist simultaneously
- Mutable References: Only one mutable reference can exist at a time
- 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)
- Prefer Borrowing: Use references when you don't need ownership
- Explicit Moves: Make ownership transfers explicit and clear
- Minimize Copies: Avoid unnecessary copying of large data structures
- Plan Resource Lifecycle: Think about when resources should be created and destroyed
- Use Abilities Appropriately: Only add abilities when necessary
Common Pitfalls
Warning 2.1 (Ownership Mistakes) Common mistakes to avoid:
- Trying to Use Moved Values: Using variables after they've been moved
- Multiple Mutable References: Creating multiple mutable references to the same data
- Dangling References: Creating references that outlive their referents
- Forgetting to Destroy: Not properly destroying resources
- 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
- Exercise 2.1: Create a function that takes ownership of a resource and returns it
- Exercise 2.2: Implement a function that borrows a value instead of taking ownership
- Exercise 2.3: Identify ownership violations in a given Move program
- Exercise 2.4: Design a struct that demonstrates different ownership patterns
References
- Move Language Specification
- Move Ownership and References Guide
- Smart Contract Security Best Practices
- 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
- Each value has a variable that's called its owner
- There can only be one owner at a time
- 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)
hascopy
, 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 duplicateddrop
: The type can be destroyedstore
: The type can be stored in global storagekey
: 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:
- At any given time, you can have either one mutable reference or any number of immutable references to a particular value
- 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:
- At any given time, you can have either one mutable reference or any number of immutable references to a particular value
- References must always be valid
- 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
Operation | Standard Vector | BigVector |
---|---|---|
Push back | O(1) amortized | O(1) |
Pop back | O(1) | O(1) |
Access by index | O(1) | O(1) |
Insert at index | O(n) | O(n) |
Remove at index | O(n) | O(n) |
Storage cost | Single slot | Multiple slots |
Parallelization | No | Yes (for table operations) |
Best Practices
- Choose the right type: Use standard vectors for small collections, BigVector for large ones
- Prefer push_back/pop_back: These are the most efficient operations
- Avoid frequent insert/remove: These operations are expensive
- Use references when possible: Borrow elements instead of copying when you only need to read
- Consider storage costs: Vectors stored in global storage consume storage slots
- 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
Operation | SimpleMap | OrderedMap | BigOrderedMap |
---|---|---|---|
Insert | O(1) avg | O(1) avg | O(1) avg |
Lookup | O(1) avg | O(1) avg | O(1) avg |
Delete | O(1) avg | O(1) avg | O(1) avg |
Iteration | Unordered | Ordered | Ordered |
Storage | Single slot | Single slot | Multiple slots |
Parallelization | No | No | Yes |
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
- Choose the right map type: Consider your data size and ordering requirements
- Use appropriate key types: Prefer simple types like
address
oru64
for keys - Handle missing keys: Always check if a key exists before accessing it
- Consider storage costs: Maps stored in global storage consume storage slots
- Plan for growth: If your map might grow large, consider BigOrderedMap from the start
- Use references: Borrow values instead of copying when you only need to read
- 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
Operation | Table | TableWithLength | SmartTable |
---|---|---|---|
Insert | O(1) avg | O(1) avg | O(1) avg |
Lookup | O(1) avg | O(1) avg | O(1) avg |
Delete | O(1) avg | O(1) avg | O(1) avg |
Length | N/A | O(1) | O(1) |
Iteration | Yes | Yes | Yes |
Storage slots | N+1 | N+2 | Variable |
Parallelization | Yes | No | Yes |
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
- Choose the right table type: Consider your specific needs for length tracking and iteration
- Use appropriate key types: Prefer simple types like
address
oru64
for keys - Handle missing keys: Always check if a key exists before accessing it
- Consider storage costs: Each table entry uses a storage slot
- Plan for parallelization: Use basic Table when you need parallel operations
- Monitor key distribution: For SmartTable, ensure keys are well-distributed
- Use references: Borrow values instead of copying when you only need to read
- Batch operations: When possible, batch multiple operations together
- Consider deletion: Use TableWithLength if you need to delete the entire table
- 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 Type | Library | Ordering | Length Tracking | Scalability | Parallelization | Storage Cost | Best For |
---|---|---|---|---|---|---|---|
Vector | std | Yes | Yes | Low | No | Single slot | Small sequences |
BigVector | aptos_std | Yes | Yes | High | Yes | Multiple slots | Large sequences |
SimpleMap | std | No | No | Low | No | Single slot | Small key-value |
OrderedMap | aptos_std | Yes | Yes | Low | No | Single slot | Ordered key-value |
BigOrderedMap | aptos_std | Yes | Yes | High | Yes | Multiple slots | Large ordered key-value |
Table | aptos_std | No | No | High | Yes | N+1 slots | Large conflicting key-value data sets |
TableWithLength | aptos_std | No | Yes | High | No | N+2 slots | Large data sets with lots of reads and low writes |
SmartTable | aptos_std | No | Yes | High | Yes | Variable | Large 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
Operation | Vector | BigVector | SimpleMap | OrderedMap | BigOrderedMap | Table | TableWithLength | SmartTable |
---|---|---|---|---|---|---|---|---|
Append | O(1) | O(1) | N/A | N/A | N/A | N/A | N/A | N/A |
Insert (index) | O(n) | O(n) | N/A | N/A | N/A | N/A | N/A | N/A |
Insert | N/A | N/A | O(1) | O(log(n)) | O(log(n)) | O(1) | O(1) | O(n) |
Access | O(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) |
Iteration | O(n) | O(n) | O(n) | O(n) | O(n) | N/A | N/A | O(n) |
Storage Characteristics
Characteristic | Vector | BigVector | SimpleMap | OrderedMap | BigOrderedMap | Table | TableWithLength | SmartTable |
---|---|---|---|---|---|---|---|---|
Storage Slots | O(1) | O(n) | O(1) | O(1) | O(n) | O(n) | O(n) | O(n) |
Parallelizable | No | *Sometimes | No | No | **Sometimes | Yes | *Sometimes | *Sometimes |
BCS serializable | Yes | No | Yes | Yes | No | No | No | No |
*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
- Start Simple: Begin with standard library collections (Vector, OrderedMap)
- Plan for Growth: If your dataset might grow large, consider scalable alternatives early
- Consider Storage Costs: Each storage slot has a cost, so choose efficiently
- Match Use Case: Choose collections that match your specific requirements
- Test Performance: Benchmark with realistic data sizes before committing
- Monitor Gas Costs: Different collections have different gas costs for operations
- Consider Parallelization: Use parallelizable collections when possible for better performance
Migration Paths
Growing from Small to Large
- Vector → BigVector: When you exceed ~1,000 elements
- OrderedMap → BigOrderedMap: When you exceed ~1,000 entries
Optimizing for Storage
- Table → SmartTable: When you want to reduce storage slot usage
- Table → TableWithLength: When you need length tracking
Optimizing for Performance
- TableWithLength → Table: When you need parallelization
- OrderedMap → SimpleMap: When you don't need ordering
Types not recommended for most use cases
- 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:
- Where will the most conflicts occur on my codebase?
- Can I use different accounts or addresses to ensure that there are reduced conflicts?
- Can I use separate storage slots (such as a table or objects) to ensure that there are reduced conflicts?
- 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:
- Account Model - Similar to the account model of Ethereum
- Object Model - Similar to the UTXO model of Bitcoin
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.
Type | Number of bytes | Description |
---|---|---|
bool | 1 | Boolean value (true or false) |
u8 | 1 | Unsigned 8-bit integer |
u16 | 2 | Unsigned 16-bit integer |
u32 | 4 | Unsigned 32-bit integer |
u64 | 8 | Unsigned 64-bit integer |
u128 | 16 | Unsigned 128-bit integer |
u256 | 32 | Unsigned 256-bit integer |
address | 32 | Aptos Address (32-byte integer) |
uleb128 | 1-32 | Unsigned 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:
Value | BCS Serialized Value |
---|---|
false | 0x00 |
true | 0x01 |
U8
A U8 is an unsigned 8-bit integer (1 byte).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x00 |
1 | 0x01 |
16 | 0x0F |
255 | 0xFF |
U16
A U16 is an unsigned 16-bit integer (2 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x0000 |
1 | 0x0001 |
16 | 0x000F |
255 | 0x00FF |
256 | 0x0100 |
65535 | 0xFFFF |
U32
A U32 is an unsigned 32-bit integer (4 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x00000000 |
1 | 0x00000001 |
16 | 0x0000000F |
255 | 0x000000FF |
65535 | 0x0000FFFF |
4294967295 | 0xFFFFFFFF |
U64
A U64 is an unsigned 64-bit integer (8 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x0000000000000000 |
1 | 0x0000000000000001 |
16 | 0x000000000000000F |
255 | 0x00000000000000FF |
65535 | 0x000000000000FFFF |
4294967295 | 0x00000000FFFFFFFF |
18446744073709551615 | 0xFFFFFFFFFFFFFFFF |
U128
A U128 is an unsigned 128-bit integer (16 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x00000000000000000000000000000000 |
1 | 0x00000000000000000000000000000001 |
16 | 0x0000000000000000000000000000000F |
255 | 0x000000000000000000000000000000FF |
65535 | 0x0000000000000000000000000000FFFF |
4294967295 | 0x000000000000000000000000FFFFFFFF |
18446744073709551615 | 0x0000000000000000FFFFFFFFFFFFFFFF |
340282366920938463463374607431768211455 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF |
U256
A U256 is an unsigned 256-bit integer (32 bytes).
Examples:
Value | BCS Serialized Value |
---|---|
0 | 0x0000000000000000000000000000000000000000000000000000000000000000 |
1 | 0x0000000000000000000000000000000000000000000000000000000000000001 |
16 | 0x000000000000000000000000000000000000000000000000000000000000000F |
255 | 0x00000000000000000000000000000000000000000000000000000000000000FF |
65535 | 0x000000000000000000000000000000000000000000000000000000000000FFFF |
4294967295 | 0x00000000000000000000000000000000000000000000000000000000FFFFFFFF |
18446744073709551615 | 0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF |
340282366920938463463374607431768211455 | 0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF |
115792089237316195423570985008687907853269984665640564039457584007913129639935 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF |
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 0
s.
Examples:
Value | BCS Serialized Value |
---|---|
0x0 | 0x0000000000000000000000000000000000000000000000000000000000000000 |
0x1 | 0x0000000000000000000000000000000000000000000000000000000000000001 |
0xA | 0x000000000000000000000000000000000000000000000000000000000000000A |
0xABCDEF (Legacy shortened address) | 0x0000000000000000000000000000000000000000000000000000000000ABCDEF |
0x0000000000000000000000000000000000000000000000000000000000ABCDEF | 0x0000000000000000000000000000000000000000000000000000000000ABCDEF |
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF |
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:
Value | BCS Serialized Value |
---|---|
0 | 0x00 |
1 | 0x01 |
127 | 0x7F |
128 | 0x8001 |
240 | 0xF001 |
255 | 0xFF01 |
65535 | 0xFFFF03 |
16777215 | 0xFFFFFF07 |
4294967295 | 0xFFFFFFFF0F |
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:
Type | Value | Encoded 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):
Type | Value | Encoded 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:
Value | Encoded Value |
---|---|
A | 0x0141 |
hello | 0x0568656C6C6f |
goodbye | 0x07676F6F64627965 |
❤️ | 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:
Type | Value | Encoded Value |
---|---|---|
Option | None | 0x00 |
Option | Some(false) | 0x0100 |
Option | Some(true) | 0x0101 |
Option<vector<u16>> | None | 0x00 |
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:
- The number of execution units, which vary based on the operations taken.
- The number of IO units, which vary based on which storage slots are read or written.
- 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:
- Fungible Tokens
- Non-fungible and Semi-fungible Tokens
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.
macOS
Homebrew (Recommended)
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
Winget (Recommended)
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 }
}
}