For developers looking to build on the Aptos blockchain, understanding Structs and Abilities in Move is not just beneficial, it's absolutely essential. These two concepts are the very bedrock upon which every Move module is constructed, especially when creating digital assets like tokens, including those conforming to the modern Fungible Asset (FA) standard on Aptos.
At its core, a Move smart contract's power lies in its ability to define and manage custom data. Structs provide the blueprint for this, allowing you to encapsulate related pieces of information into organized, meaningful data types. Without them, you'd be limited to basic primitive types like booleans, and addresses, which simply isn't enough to model complex real-world assets or application states.
Intrinsically linked to structs are Abilities. These are keywords that dictate the fundamental properties and behaviors of your custom data types. Abilities define how a struct can be used, copied, stored, or dropped, acting as crucial guardrails for Aptos Move's famed resource safety guarantees. Every struct you define will inherently possess (or lack) these abilities, directly impacting how it interacts within your module and across the blockchain's global state.
This guide will simplify structs and abilities, making them relatable and easy to understand without sacrificing technical accuracy. We'll explore how structs are defined, what each ability signifies, and crucially, how they collaborate to enable the secure and efficient management of digital assets. You'll learn how these concepts underpin the creation of tokens, where assets are often represented as special structs called "resources”: unique data types that cannot be duplicated or implicitly discarded. By the end, you'll have a solid grasp of how to leverage structs and abilities to write safe, composable, and efficient smart contracts on Aptos, helping you to build compelling dApps and define your own valuable digital assets. Let’s get in!
Structs: Your Custom Data Blueprints
As mentioned, a struct in Move is like a custom blueprint or a template for creating your own data types. It allows you to group related pieces of information, called "fields," into a single, organized unit. It is similar to structs in other languages like Rust or C, but with stronger restrictions in Move due to its resource-oriented model.
We'll go through four stages:
- No abilities → won't compile for most real-world usage
- Add drop → can discard
- Add store → can store in global storage
- Add key → can publish under an account
Minimal Token struct
- No Abilities
Let's start with a simple, no-abilities struct: defining a Token struct. Note that a struct must be defined inside a module
module my_address::token_copy {
use std::signer;
use std::string::{Self, String};
struct Token {
name: String,
supply: u64,
}
//functions go here
}
In this example:
struct Token { ... }
declares a new struct named Token
. It holds a name
, which is the name of the token, and a supply: u64
, which defines the total supply of the token as an unsigned 64-bit integer.
Any time you create an instance of Token
, it will consistently have these two fields, ensuring that your data is always well-structured.
Abilities: The Permissions and Powers of Your Data
If structs are the blueprints for your data, abilities are the rules or permissions that dictate how instances of those structs can be handled by the MoveVM. They are fundamental to Move's security model, especially its "resource safety" guarantees. Each struct you define can be declared with one or more of four core abilities.
The Four Core Abilities
The drop
ability
drop
is a permission that allows a struct to be automatically discarded when it goes out of scope (like when a function ends). Without it, the Move compiler forces you to explicitly handle the value, meaning you must either move it somewhere useful or destroy it manually.
It is used for memory management during execution only… not related to publishing or storage.
What happens with vs without drop
First, we'll create/instantiate a Token
struct without the drop
ability to observe how Move enforces explicit value handling
module my_address::token_drop {
use std::signer;
use std::string::{Self, String};
struct Token {
name: String,
supply: u64,
}
public entry fun initialize(admin: &signer, name: String, supply: u64) {
let token = Token {
name,
supply,
};
}
}
Without drop, the Token
cannot be silently discarded when the function ends, hence the error below. You must either transfer ownership or explicitly destroy it.
To fix this, we have to add drop
ability to the token struct.
module my_address::token_drop {
use std::signer;
use std::string::{Self, String};
struct Token has drop {
name: String,
supply: u64,
}
public entry fun initialize(admin: &signer, name: String, supply: u64) {
let token = Token {
name,
supply,
};
}
}
Ideally, a token should not be droppable.
This will compile with the result below:
The copy
ability
The copy
ability in Aptos Move is a fundamental feature that determines whether a struct can be duplicated. It plays a crucial role in how data is handled, ensuring safety and preventing unintended behavior in smart contracts.
It allows a struct to be explicitly duplicated using the copy keyword. Without copy, a struct cannot be copied; it can only be moved (that is, ownership transferred).
What happens with vs without copy
Below is an example of code without the copy ability.
module my_address::token_copy {
use std::signer;
use std::string::{Self, String};
struct Token has drop {
name: String,
supply: u64,
}
fun consume_token(t: Token){}
public entry fun initialize(admin: &signer, name: String, supply: u64) {
let token = Token {
name,
supply,
};
consume_token(token);
consume_token(token);
}
}
Since Token
is not copy
able, calling consume_token(token)
moves the value. After this call, token
can’t be used again because its ownership has been transferred.
token
is no longer valid in this scope.The memory that held
token
's data is now owned by the firstconsume_token
.
An attempt to pass token
again will result in error, and will not compile.
See the error message here:
Now let's add copy
ability to Token.
module my_address::token_copy {
use std::signer;
use std::string::{Self, String};
struct Token has drop, copy {
name: String,
supply: u64,
}
public entry fun initialize(admin: &signer, name: String, supply: u64) {
let token = Token {
name,
supply,
};
consume_token(token);
consume_token(token);
}
fun consume_token(t: Token){}
}
Ideally, a token shouldn't be copyable.
Here we go. Compiled successfully.
You can also make the copy operation explicit by writing
consume_token(copy token)
, which indicates that thetoken
value should be duplicated before being passed to the function.
The key
ability
In Aptos Move, a struct becomes a resource when it has the key ability (and typically also store). This combination marks the struct as a persistent, account-bound data type that can be stored in global storage. Here's the definitive explanation:
struct Token has key, store { // this is a resource
name: String,
supply: u64
}
If a struct has the key ability, it automatically has the store ability, even if it's not explicitly declared.
The key
ability implies the struct, in this case Token
, can be stored in global storage, and can be accessed from same. This makes Token
a Resource.
Key Characteristics of Resources:
-
Stored in Global Storage
- Lives under an account (move_to(account, resource))
- Accessed via borrow_global/move_from
-
Singular Per Account
- Only one instance per (account_address, struct_type) pair
-
Explicit Lifetime Management
- Must be explicitly stored/retrieved/deleted
- Cannot be implicitly dropped or copied (no drop ability)
Global Storage
Operations around global storage are critical to the functioning of the key
ability.
Global Storage is the on-chain storage where values are persistently stored at an account address. Unlike local variables that disappear after a function finishes running, global storage persists across transactions.
Global Storage Operators
Image source: Aptos docs
These operators all work with key
ability.
Example
module my_address::token_key {
use std::signer;
use std::string::{Self, String};
struct Token has key {
name: String,
supply: u64,
}
public entry fun initialize(admin: &signer, name: String, supply: u64) {
let token = Token {
name,
supply,
};
move_to(admin, token); // move the token to the admin's account
}
}
The above code compiles successfully.
move_to(admin, token);
Stores the token under the admin's account in global storage. This is why Token needs the key ability.
Acquires
The acquires
keyword in Move is used to explicitly declare that a function may access or modify a resource type stored in global storage. It's a safety feature that ensures the compiler knows which resources a function interacts with.
This annotation tells the Move compiler that this function will access the
Resource
from global storage-
It's required whenever a function:
- Reads a resource (borrow_global)
- Modifies a resource (borrow_global_mut)
- Removes a resource (move_from)
Example using the acquires
keyword. Note this is a basic code.
module my_address::token_key {
use std::signer;
use std::string::{Self, String};
struct Token has key {
name: String,
supply: u64,
}
public entry fun initialize(admin: &signer, name: String, supply: u64) {
let token = Token {
name,
supply,
};
move_to(admin, token); // move the token to the admin's account
}
//use acquires
public fun get_balance(addr: address): u64 acquires Token {
let token = borrow_global<Token>(addr);
token.supply
}
}
In the code above, acquires
grants the get_balance
function access to the Token resource.
The borrow_global<Token>(addr);
gets an immutable reference to the Token resource stored at addr, returns a reference that allows reading but not modifying.
Acquires is optional in Move 2.x as the compiler can now infer this annotation when necessary
We'll dive deeper into other global operators in our subsequent classes.
The store
ability
And now, to store
ability.
According to the aptos move docs,
The
store
ability allows values of types with this ability to exist inside a struct (resource) in global storage, but not necessarily as a top-level resource in global storage. This is the only ability that does not directly gate an operation. Instead, it gates the existence in global storage when used in tandem withkey
.
This is straightforward.
module my_address::token_store {
use std::signer;
use std::string::{Self, String};
struct Token has key, store {
name: String,
supply: u64,
}
struct Vault has key {
token: Token,
}
/// initialize a Vault that wraps a Token and store it in the signer's account
public entry fun initialize(admin: &signer, name: String, supply: u64) {
let token = Token {
name,
supply,
};
let vault = Vault {
token,
};
move_to(admin, vault);
}
public fun get_token_supply(addr: address): u64 acquires Vault {
let vault = borrow_global<Vault>(addr);
vault.token.supply
}
}
If we try to compile this without the store
ability on Token
, the compiler will throw an error because storing a value inside another struct requires the store
ability. You can try removing it to see the error for yourself.
Struct Ability Requirements in Aptos Move
Golden Rule:
When a struct has an ability, all its fields must have:
Why This Matters:
copy+drop = Temporary data
key+store = Global storage resources
Prevents unsafe combinations at compile time
Conclusively, remember these constraints aren't limitations. They are Move's way of eliminating whole classes of bugs at compile time. Whether you're storing assets globally with key or working with temporary copy data, these rules ensure your contracts behave predictably. Now that you understand how abilities propagate through structs, you're ready to design safer, more efficient Move modules.
How will you apply these rules in your next smart contract?
You can access the examples on github.
You can also reach me on Twitter now X.
Top comments (0)