MOVE demystified part 2 — structs, storage and (cap)abilities

Monethic.io
4 min readMay 24, 2024

--

In the previous part, we discussed how to deploy your first module. In this part, we’ll take a closer look at how data is kept and managed in Move. Let’s start by considering a very basic thing — a module declaration —such as below example:

module 0x1234::Hello

you may think, what is this? So, let us introduce how storage works in Move.

Aptos Move Storage

In Move, the storage of data on the blockchain is done via a concept called Resources and Modules. We already know what is a Module, as we coded it — it is a smart contract, or rather a bytecode. An account on Aptos blockchain may contain many Modules and Resources.

https://github.com/aptos-labs/aptos-core/raw/main/aptos-move/move-examples/move-tutorial/diagrams/move_state.png

What is a Resource? In short words, a piece of data. Similarly like storage variables in Solidity, on Aptos we have resources that may hold some information. But, those resources are subject to specific rules, which ensure their safety. For example, in the previous HelloWorld module, what we did is we stored the newly created module on our own account. But how it works with data?

Global storage can be described as a simple key-value storage where key is a name of property and value is what’s stored. In Move, you do not use standalone global storage variables like u64 directly. Instead, everything that is stored in global storage must be encapsulated within a struct.

Abilities

As there are some resources held in the storage, some methods have to exist to manage them.

In Solidity, the storage is managed per each smart contract and there is no any “special” management logic. Simply if you declare e.g.

mapping (address => uint) balances;

as a storage variable, you can reference it whenever you want, no magic here.

But, since Move is Rust, which is a memory-safe language, you cannot just pick up or reference random objects that easily!

In Move, struct declarations can have up to four abilities that define how instances of these types can be used, dropped, or stored.

Struct Declaration

A typical struct (which is an exemplary resource) declaration in Move looks like this:

struct NAME has Abilities {
FIELD1: TYPE1,
FIELD2: TYPE2
}

For example:

struct Counter has key {
value: u64
}

This indicates that Counter can be used as a key for global storage operations. What does it even mean can be used as key? Resources in Global Storage can be manipulated using 4 types of abilities, and key is one of them. Below table explains their usage:

Move abilities

Acquires Annotation

Whenever a function interacts with global storage using move_from, borrow_global, or borrow_global_mut, it must include the acquires annotation in its signature to indicate it accesses global resources. For example:

public fun get_value(addr: address): u64 acquires Counter {
borrow_global<Counter>(addr).value
}

In some cases, specified in move docs here, it is required to use acquires annotation — as per the mentioned docs:

A Move function m::f must be annotated with acquires T if and only if:

· The body of m::f contains a move_from<T>, borrow_global_mut<T>, or borrow_global<T> instruction, or

· The body of m::f invokes a function m::g declared in the same module that is annotated with acquires

MOVING in MOVE

Speaking of global storage operations, you need them to access or modify the global storage. Move programs can create, delete, and update resources in global storage using the following five instructions. Of course, important part of all this is access control, you have to be authorized to interact with desired resources.

Move global storage operations

If the table sounds unclear, here are some examples:

  • move_to can be used to mint a Coin to an user
  • move_from can be used to burn a Coin from an user
  • borrow_global allows for using some variable, read only mode
  • borrow_global_mut allows for using some variable, write mode
  • exists is just an existence check

Finally, the Capabilities

There is a pattern named capabilities in Move, which is strictly related to above features. It can be used to enforce access control. Capabilities are special resources that grant specific privileges or permissions to an account. They are implemented using structs.

module 0x1::ExampleModule {
use std::signer;

// Define the capability struct
struct MintCapability has key {}

// Function to grant the capability to an account
public fun grant_capability(account: &signer) {
move_to(account, MintCapability {});
}

// Function requiring the capability to mint tokens
public fun mint_tokens(account: &signer, amount: u64) acquires MintCapability {
assert!(exists<MintCapability>(signer::address_of(account)), 1);
// Logic to mint tokens...
}
}

If that still look confusing, consider below Solidity snippet:

modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}

function grantRole(address account) public onlyOwner {
roles[account] = true;
}

function mintTokens(uint256 amount) public {
require(roles[msg.sender], "Not authorized");
// Logic to mint tokens...
}

They are similar to each other, aside from the fact that for example in Move, you can Move (no pun intended) the capability to someone else, while in Solidity, you will just change the storage variable.

In the next article, not to make this one too lengthy, we’ll discuss security risks related to Resources, Abilities and Capabilities. As soon as it’s finished, the link will appear here. Stay tuned!

--

--

Monethic.io

We are providing security services for smart contracts & web3. Find us on twitter https://twitter.com/Monethic_io