DEV Community

Cover image for Understanding Structs and Abilities in Move: The Building Blocks of Aptos Smart Contracts
Daniel
Daniel

Posted on • Edited on

Understanding Structs and Abilities in Move: The Building Blocks of Aptos Smart Contracts

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:

  1. No abilities → won't compile for most real-world usage
  2. Add drop → can discard
  3. Add store → can store in global storage
  4. 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
}
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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.

Compilation error message showing code wouldn't work without the drop ability in aptos move

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

}
Enter fullscreen mode Exit fullscreen mode

Ideally, a token should not be droppable.

This will compile with the result below:

Code compiles successfully with drop ability

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

}
Enter fullscreen mode Exit fullscreen mode

Since Token is not copyable, 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 first consume_token.

An attempt to pass token again will result in error, and will not compile.

See the error message here:

Compilation error message showing code wouldn't work without the copy ability in aptos move

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){}
}
Enter fullscreen mode Exit fullscreen mode

Ideally, a token shouldn't be copyable.

Here we go. Compiled successfully.
Successful compilation message

You can also make the copy operation explicit by writing consume_token(copy token), which indicates that the token 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
}

Enter fullscreen mode Exit fullscreen mode

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 from aptos docs showing the different 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
    }   
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 with key.

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

Enter fullscreen mode Exit fullscreen mode

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:

A simple diagram showing different behaviours of structs with respective abilities in aptos move

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)