Code in Move [1] — Aptos Smart Contracts Basics

Thouny
7 min readNov 23, 2022

--

Aptos Labs Banner

Aptos Autumn is already over ? Only for the opportunists, now it’s time to build! So, in this second article, we will get to work by writing and deploying a first Smart Contract on Aptos. The goal of this one will be to issue a Coin, then mint an amount of it directly in the wallet and finally burn some. You can find the final code on my Github. If you are just discovering Move programming language I suggest you have a look at the first article of the series.

Prerequisites

Personally, I will use Ubuntu and VSCode in this tutorial but you can also use macOS or Windows and IntelliJ from JetBrains to code in Move on Aptos (however IntelliJ does not have a plugin for Sui).

First of all you will need Aptos CLI, install it by following the instructions here. Install Rust in stable version or if you already have it in nightly, switch it to stable version: rustup default stable. You can then install move-analyzer extension in VSCode. Finally, in your workspace create an Aptos folder and clone aptos-core in it.

On Aptos, Smart Contracts are actual packages divided into modules. These modules are deployed (or published) under the account of the publisher (we will see later that this poses accessibility problems and the solutions that are available to us). The Aptos framework is also composed of modules published at address 0x1. You can find its content here.

Now we can create a project, it is structured as follows: a source folder which will contain our modules and a Move.toml file which will contain our addresses and dependencies.

Project structure

Copy this in the Toml file. The addresses section allows you to give a name to the addresses used in the module. For the moment there is only the address under which the module will be published. In dependencies we import the Aptos framework either directly from Github or locally so that move-analyzer reacts faster.

[package]
name = "CoinMint"
version = "0.0.0"

[addresses]
coin_mint = "your_address_here"

[dependencies]
AptosFramework = { local = "/home/user/.../Aptos/aptos-core/aptos-move/framework/aptos-framework" }
# AptosFramework = {git = 'https://github.com/aptos-labs/aptos-core.git', rev = 'devnet', subdir = 'aptos-move/framework/aptos-framework'}

Then, in your .move file, write this. Here we define the module name (coin_coin) and we import everything we need from the Aptos Framework. Be careful, coin_mint is not the package name but the named address under which the module will be published.

Writing the Smart Contract

module simple_aptos_coin::coin_coin {
// Imports
use std::error;
use std::signer;
use std::string;
use aptos_framework::coin;
}

Below we define some errors and Resources that we will use.

    // Errors
const ENOT_ADMIN: u64 = 0;
const ENO_COIN_CAP: u64 = 1;
const EALREADY_COIN_CAP: u64 = 2;


// Resources
struct CoinCoin has key {}
struct Capabilities has key {
mint_cap: coin::MintCapability<CoinCoin>,
burn_cap: coin::BurnCapability<CoinCoin>,
}

Let’s talk about the different functions in Move. By default, they are private so you have to add public so that they can be accessed by other modules. However, this is not enough to be triggered by a user, you must also indicate that it is an entry function (be careful, these functions cannot return values). Then, when the module is published, the init_module function is automatically called. I advise you to use it as little as possible because it behaves oddly when upgrading a module. To learn more about the mutability of contracts, read this.

We create an empty init_module function and another one for issuing a coin. When issuing the Coin we define its name, symbol, decimals number, whether we want to monitor the supply and the account that will store this information. Then we get back burning, freezing and minting capabilities (which are Resources) that we can store under our account or destroy explicitly. For example, by destroying the mint_cap you won’t be able to mint coins anymore so the supply is fixed.

An interesting point to note here is that an account can only have one Resource of a type at a time. That’s why we check if Capabilities already exist. Also init_module always take the signer as an argument, but since we don’t use it we write _.

fun init_module(_: &signer) {}

public entry fun issue(account: &signer) {
let account_addr = signer::address_of(account);
// we verify that we didn't already issued the Coin
assert!(
!exists<Capabilities>(account_addr),
error::already_exists(EALREADY_COIN_CAP)
);

let (burn_cap, freeze_cap, mint_cap) = coin::initialize<CoinCoin>(
account,
string::utf8(b"Coin Coin"),
string::utf8(b"COINCOIN"),
18,
true
);
move_to(account, Capabilities {mint_cap, burn_cap});
// we don't ever want to be able to freeze coins in users' wallet
// so we have to destroy it because this Resource doesn't have drop ability
coin::destroy_freeze_cap(freeze_cap);
}

The Coin Coin is now initialized, so the next step is to mint an amount. Unlike other blockchains, Aptos has an extra layer of protection against spam and scam assets. In order for an account to receive a Coin, it must first register it by creating a CoinStore (a Resource that stores a type of Coin in the account). This feature improves the user experience and allows wallets to receive only the assets they want. We will add it directly in the mint function.

In this function, we first check if the user has the Capabilities resource for CoinCoin. If not, it means that the user trying to mint didn’t issue the Coin, so we abort. Then we need to get the mint_cap from the user account by using borrow_global. The we can mint, register and finally deposit the coin to the account (deposit is a shortcut for transfer & merge Coin). Read this to understand why we need to merge Coins.

public entry fun mint(user: &signer, amount: u64) acquires Capabilities {
let user_addr = signer::address_of(user);
// we check if the user already issued the Coin
assert!(exists<Capabilities>(user_addr), error::permission_denied(ENO_COIN_CAP));

// we need to get the mint_cap from the user account
let mint_cap = &borrow_global<Capabilities>(user_addr).mint_cap;
let coins = coin::mint<CoinCoin>(amount, mint_cap);
coin::register<CoinCoin>(user);
coin::deposit<CoinCoin>(signer::address_of(user), coins);
}

As you may have noticed, we need to add acquires. This is because we are calling borrow_global and, as a security reasons, we must indicate that we actually want to “take” the Resource from the user’s account. This is also required with borrow_global_mut and move_to.

Here is the last function of this first smart contract, burn, very similar to mint.

public entry fun burn(user: &signer, amount: u64) acquires Capabilities {
let user_addr = signer::address_of(user);
assert!(exists<Capabilities>(user_addr), error::permission_denied(ENO_COIN_CAP));

let burn_cap = &borrow_global<Capabilities>(user_addr).burn_cap;
let coins = coin::withdraw<CoinCoin>(user, amount);
coin::burn<CoinCoin>(coins, burn_cap);
}

You can find the final code on my Github if needed.

Publishing and interacting with the module

Our module is now ready to be deployed, but before doing so we need to init our Aptos account with the CLI using aptos init in our Aptos folder. Choose devnet, then enter the private key of the address you added in Move.toml or press enter to create a new account (you will need to change the address in the Toml file in this case).

After adding our account we can compile our smart contract with aptos move compile in the project folder (Tip: alias am="aptos move" to use am as a shortcut in the terminal).

You should obtain a similar result with b44… being the address you wrote in Move.toml. Finally deploy the contract with aptos move publish.

Go to the explorer under your address to see the deployment transaction, the module and the Resources created. There are three Resources at the moment: the account, the registry for the packages the account publishes and the CoinStore for $APT.

Now let’s interact with our Smart Contract by calling the issue function. aptos move run --function-id 0xyouraddress::coin_coin::issue. If you try to run it again you should see an error: EALREADY_COIN_CAP(0x80002)

Next, we can mint some CoinCoins with aptos move run --function-id 0xyouraddress::coin_coin::mint --args u64:100. And same with burn. You can see the changes in the account Resources. CoinInfo is a Resource tracking the total supply and the CoinStore holds the CoinCoins you own.

Summing up

That’s it for this second article on the Move programming language. You already saw some of his security perks and there are many more. Those protections have been designed to help developers to manipulate true virtual assets. You can’t delete a Coin by accident (try removing the deposit, the compiler will throw an error) and you can’t create Coins by magic (it’s impossible to copy a Coin, you need to have the mint_cap to call the mint function). This particularity among others makes Move one of the best Smart Contract programming language.

Obviously there is one (even two) extremely important step missing in this tutorial, the testing part before deployment. We should pursue tests with the Aptos framework throughout our development and formal verification with Move Prover. But don’t worry we will see that in a future article.

Moreover, if you want to mint a Coin using another account you won’t be able to do so with this module. We will see the solution proposed within the Aptos Framework in the next article.

If you have any questions, feel free to drop a dm on Twitter. And follow me to discover new things on the blockchain!

--

--

Thouny

Blockchain developer crafting educational and technical content on Web3 techs and philosophies. Digressions on life trying to make sense of it.