Aptos Move: Struct and its Abilities Explained — has Drop, Copy, Key, Store

Moncayo Labs
9 min readOct 4, 2023

--

Teaching disclaimer: All code snippets presented here compile. We do not believe broken code snippets are an effective way to teach new concepts. If you want to convince yourself of what is being explained here, we recommend copy pasting the code snippets into an editor and playing around with them yourself.

Today we are going to learn about structs in Aptos Move. A struct is a user-defined data structure in Aptos Move. Struct are a unique feature of Aptos Move and distinguish it from SUI Move and its predecessor Rust (used on Solana). In Aptos Move we have structs with 4 possible abilities has key, store, drop, copy.

In Aptos Move, we can imagine any module we build to be a component sitting in a global code base and the 4 abilities determine how the data within a given module is stored, used, accessed, and distributed. This way Aptos smart contract development is very intuitive than smart contract development in Rust. Furthermore, Aptos Move web3 development more closely matches coding in the web2-universe. In short, Move structs are cool and let’s find out why.

What is a struct?

A struct is the data type to store stateful smart contract data in. In other words, it is a custom data type that stores information beyond contract execution. It can contain attributes with primitive types (unsigned integers, strings, etc.) or in turn other structs or custom types.

SUI Move supports objects and classes as well, but Aptos Move, the version of Move we are focussing on today, does not. So “struct” is all we got.

struct has drop, key, copy, store…

The “naked” struct

module publisher::my_super_easy_module {
use std::signer;
use std::string::{Self, String};
use std::debug;

struct Person {
name: String,
age: u64
}
}

The default struct without any abilities (the Person struct above), let’s call it a “naked struct”, can only be accessed within the module that the struct is declared in i.e. here it is the my_super_easy_module. The keys name and age within the Person struct can only be accessed from within the module.

Note that structs can contain other structs as their keys, but they can never be recursive. Thus: struct Foo { x: Foo } will not compile.

An instance of a “naked struct” cannot be copied, cannot be dropped (deleted), and cannot be stored in global storage.

Let’s say we have a struct without any abilities:

public entry fun pass_through(person: Person): Person {
let new_person = person;
return new_person
}

This code compiles because we are literally only passing an instance of the “naked struct” from one function to the next. We are not creating (key) or deleting (drop) an instance of the struct in any parts of the code.

The 4 Abilities

If we wanted to actually turn our struct into anything useful, we’ll have to add abilities. Abilities determine the permitted behaviour for the defined data struct, e.g. how values in the struct may be moved, stored or accessed.

According to Aptos Move abilities definitions the 4 abilities are formally defined as:

  • copy: allows values of types with this ability to be copied.
  • drop: allows values of types with this ability to be popped/dropped.
  • store: allows values of types with this ability to exist inside a struct in global storage.
  • key: allows the type to serve as a key for global storage operations.

Let’s dive into each ability in more depth so we’ll make sure we understand them perfectly.

Drop ability enables deletion of struct instance i.e. instance can go out of scope.

Drop

If we took our “naked struct” of type Person and actually tried to create an instance of it new_person = Person{ name: b"Hugo", "age": 93} inside our initialize() function, the compiler would complain that the instance cannot be deleted. This might sound like a confusing error, as we are not actively deleting anything, just creating an instance. However, in Move (and thus Rust and C languages) as soon as we leave the scope of a function, the variables inside that scope become inaccessible in memory, which is essentially the same as the data structure being deleted.

Hence, if the Person struct does not have a drop ability, we mean that the instance of Person called new_person cannot go out of scope, it has to remain forever accessible and in scope. The Person instance could remain in scope if it was passed on to another function beyond the initialize() function where it was defined. However, in the code below the new_person instance is not passed on, hence the new_person will be deleted as soon as the initialize() function has finished executing.

To fix this code, we can simply add the has drop ability. This results in this “droppable struct”. Now the code compiles and all is well

module publisher::my_super_easy_module {
use std::signer;
use std::string::{Self, String};
use std::debug;

struct Person has drop {
name: String,
age: u64
}

public entry fun initialize(admin: &signer, name: String, age: u64){
let new_person = Person {
name: name,
age: age
};
}
}
Key ability enables the struct to be stored in global storage.

Key

Another way to create an instance of a struct (but without adding the drop ability) is to add the key ability. However, the has key ability alone won’t compile, unless you also use the move_to(admin, person) operation. Why is that?

Let’s start from what key means: Key means that the struct can be globally accessed via the global storage of the Aptos blockchain using a unique identifier. With the key ability all possible global storage operations can be performed on this type of struct (such as move_to, borrow_global, borrow_global_mut, move_from, etc.).

Having the key ability alone does not automatically write the new_person instance to global storage as it is created. It is a local instance until the move_to() command is called, where we essentially dump the instance to the global storage and leave it there. Thus, the new_person will never go out of scope and cannot be deleted.

Then if we wanted to access that globally stored instance again, we could use another global storage operation that is only permitted for structs that have key, such as borrow_global or borrow_global_mut, depending on whether we want to view the instance or modify it.

So in a nutshell, the has key ability enables an instance of type Person to be stored in the global storage. The move_to() actually pushes an instance of this globally defined struct onto the global storage. The borrow_global() gets it from the storage as an immutable type and borrow_global_mut() retrieves the mutable instance.

If we tried to use the move_to() command without the has key, the Move compiler would tell us that we are not allowed to move an instance of this type to the global storage because it does not have the key ability.

module publisher::my_super_easy_module {
use std::signer;
use std::string::{Self, String};
use std::debug;

struct Person has key {
name: String,
age: u64
}


public entry fun initialize(admin: &signer, name: String, age: u64){
let new_person = Person {
name: name,
age: age
};

move_to(
admin,
new_person,
);
}
}

Acquires

Whenever we write a function in Move that uses any of these three global storage operations (move_from<Person>, borrow_global_mut<Person>, or borrow_global<Person>), we’ll have to add an acquires Person to its function signature. Similarly, if a function calls another function in its body that uses any of these operations, we’ll have to mark the function signature with acquires Person to highlight the fact that we are interacting with the globally stored instance.

/// Reset the age of the person
public fun set_age(account: &signer, new_age: u68) acquires Person {
let person_mut = borrow_global_mut<Person>(signer::address_of(account));
person_mut.name = new_age
}

Please note that the move_to<Person> does not trigger an acquires Person in its function signature.

Copy ability enables unlimited number of by value copies of a given struct.

Copy

Let’s assume that inside our initialize() function, we created the new_person Person instance and we now pass it to another function pass_through(new_person). If we then tried to access the new_person instance in a line below the pass_through(new_person) it would not work, because the new_person left the scope and was passed on to the pass_through() function scope. But what if we wanted to pass the new_person to the pass_through() function and at the same time keep using it in the original initialize() function scope? Well, we need a copy.

Copying an instance of a struct by value to another function is only permitted if the struct has copy. If our Person struct has copy, we can simply write pass_through(copy new_person). This way, we pass the new_person instance by value i.e. a copy of that instance is passed to the pass_through() function while the original instance can still be used in the original function.

Thus by passing a copy inside a function call pass_through(copy person) Move allocates memory to the copy of the person object and thus creates a deep copy of the person object. Thus, refrain from using this excessively as it drastically increases the memory required by the module and thus makes the specific blockchain operation a lot more gas costly.

If you’re creating a new fungible asset, i.e. a new custom coin or token on Aptos, you need to make sure to not have the copy ability for your token struct. Otherwise anyone could just copy your coin and create more and more of it.

Make sure you understand: If the struct didn’t have the copy functionality. Then, the instance would not be copied by value to the pass_through() function. Instead the default way of passing a struct to a function call is pass_through(move new_person). Thus by default, the actual original instance would be passed to the pass_through() function. Now, if we wanted to use the new_person instance after the pass_through(move person) function call, we coulnd’t because it would have already left this scope.



module publisher::my_super_easy_module {
use std::signer;
use std::string::{Self, String};
use std::debug;

struct Person has drop, copy {
name: String,
age: u64
}


public entry fun initialize(admin: &signer, name: String, age: u64){
let person = Person {
name: name,
age: age
};

pass_through(copy person);
let new_person = person;
}


fun pass_through(person: Person): Person {
let new_person = person;
new_person
}
}
Store ability allows that the given struct can be stored as a key (field) of another struct inside global storage.

Store

Finally, store is the only ability that does not directly mean an additional operation can be performed with the struct. It is more of an indirect ability.

Has store simply means that an instance of Person type could be stored as a key inside another struct (resource) in global storage eg. the struct Spaceship = { driver: Person, capacity: u64 }, but not necessarily as a top-level resource in global storage.

Hang on. If you read the key section carefully, you might think, well this means that if a struct has key all its keys (attribute fields: e.g. name, age) automatically have store because they are stored on the global storage, but not as a top-level resource (its own struct). You are absolutely right. This is what it means. If a struct has key all its key-value pairs automatically have store.

Key is the only ability with this indirect relationship with respecto to its struct keys (or fields). All others behave normally:

if a struct has ability..

  • copy, all its fields must have copy.
  • drop, all its fields must have drop.
  • store, all its fields must have store.
  • key, all its fields must have store.
  • key is the only ability currently that doesn’t require itself.

Here’s an example use case of store:

module publisher::my_super_easy_module {
use std::signer;
use std::string::{Self, String};
use std::debug;

struct Person has key, store {
name: String,
age: u64
}

struct Spaceship has key {
driver: Person,
capacity: u64,
}

public entry fun initialize(admin: &signer, name: String, age: u64){
let new_person = Person {
name: name,
age: age
};

let apollo11 = Spaceship {
driver: new_person,
capacity: 100
};

move_to(
admin,
apollo11,
);
}
}

We hope this was useful to help you remember and understand the 4 abilities in Aptos Move.

Please find the example modules on our github: github.com/moncayolabs. If you have any questions or suggestions, please leave them in the comments below and we’ll try to address them as soon as possible.

If you’ve made it this far, congratulations! Please don’t forget to applaud yourself and the poor researchers pulling this info together! 🙂 👏

Follow MoncayoLabs for more Aptos Move Learning Content! 🖥 👩‍🎓

--

--

Moncayo Labs

Active Supporter of the Aptos Move Movement | Web3 Move Development Tutorials