Write Your First Smart Contract On Aptos — A Step-by-Step Guide

Samundra Karki
MokshyaProtocol
Published in
8 min readFeb 27, 2023

Introduction

This blog is created to help you start writing smart contracts in Aptos Blockchain. The blog will explain key concepts along the process. We will be writing a “token-vesting” program along the way. The “Token-Vesting” program was the first open-source contract from Mokshya Protocol. You can view the complete code here.

Mokshya is an open-source protocol building smart contracts, SDKs and developers tools on the Aptos Blockchain.

Installation and Aptos

The documentation in Aptos is pretty clear in terms of installation. You can visit here. For more guided and clear instructions for getting started begin from here.
You can study more concepts involved in Aptos here.

What is Token Vesting?

Aptos defines commonly known and used tokens as “coins” and its native token- “APT” is defined in the aptos_coin module. You can create, mint, freeze, transfer, and destroy your own tokens through the coin module.

In Aptos, Modules are like smart contracts. We can create and publish “modules”. These modules are independent entities with logic that can be called from the frontend for executing various transactions.

Now, Let’s say Alice has given work worth 1000 tokens (coin) named Mokshya “MOK” which he has to pay to Bob on a different schedule as below.

  1. Jan 1, 200 MOK
  2. Jan 30, 300 MOK
  3. Feb 12, 400 MOK
  4. Feb 21, 100 MOK

Bobs needs security that his payments will be made during these times. Alice fears if he makes all the payments at the start then Bob might not complete the work.

One solution is to find someone both of them trust to make the scheduled payment as required. A much better solution will be to use a contract that will make these scheduled payments. This is the fundamental concept of token-vesting.

Token Vesting is the trustless scheduled payment from one party to another party.

How Does Token Vesting Work?

Alice sends all the schedules with the release amount to the Aptos blockchain which is saved. Along with that Alice deposits the 1000 MOK tokens in a resource account, which acts as a trustless escrow. When the schedule comes Bob can withdraw the designated amount. From this we are clear we need two major functions:

1. To create vesting: Define the schedule and payments, receiver and deposit total amount. function create_vesting

2. To receive the payments: Where the receiver is verified and the amount due on the date is paid. function release_fund.
In the next steps, we will implement these functions

Initializing and Importing Dependencies

First of all, create a folder named token-vesting. Now, through your terminal inside the folder token-vesting, use the following command:

 aptos move init --name token-vesting

If you open the token-vesting folder you will see the following automatically generated:

This is the development structure of the Aptos module or smart contract development. In the sources folder, the module is present. An individual module can be broken down into smaller sub-modules as well, all of them are in the sources. Move.toml is similar to the package manager Cargo.toml. It defines the module name and various dependencies.

For ease replace the Move.toml with the following:

[package]
name = 'token-vesting'
version = '1.0.0'


[addresses]
token_vesting = "_"
Std = "0x1"
aptos_std = "0x1"

[dependencies]
AptosFramework = { local = "../../aptos-core/aptos-move/framework/aptos-framework"}

In place of— local = “../../aptos-core/aptos-move/framework/aptos-framework”, Use the local folder where your aptos-framework is located.

Now, you are ready to shift towards writing a smart-contracts. In the sources folder, create a file named “token-vesting.move ”.

Defining

In the starting, we define the program with module identifier with module name “ token_vesting”. The module name must match the name in the addresses of a segment of Move.toml. As explained earlier, we can write multiple sub-modules within token_vesting module, in this case we have a single sub-module named vesting.

module token_vesting::vesting {
}

Inside the module, we define all the dependencies we will be requiring in the module,


use std::signer;
use aptos_framework::account;
use std::vector;
use aptos_framework::managed_coin;
use aptos_framework::coin;
use aptos_std::type_info;
use aptos_std::simple_map::{Self, SimpleMap};

Defining the Structure

We need to save the data regarding the vesting contract for that Aptos provides the struct option which can be used to define various data structures. In our case,

// All the information required for Vesting
struct VestingSchedule has key,store
{
sender: address,
receiver: address,
coin_type:address,
release_times:vector<u64>, //The times for unlocked
release_amounts:vector<u64>, //The corresponding amount for getting unlocked
total_amount:u64, // Sum of all the release amount
resource_cap: account::SignerCapability, // Signer
released_amount:u64, //Sum of released amount
}

In Aptos, the address is the unique identifier of each account. We require the address of sender, receiver and coin_type. If you come from a background in Solana, you can take this as a “token mint address”. For our case, MOK coin is required coin_type.

release_times is the vector of the UNIX timestamp in ascending order. The UNIX timestamp corresponding to Alex and Bob's case will be (taking the year as 2023 and time of 00:00)

  1. Jan 1–1672510500
  2. Jan 30–1675016100
  3. Feb 12–1676139300
  4. Feb 21–1676916900

release_amounts is the scheduled amount at corresponding times.

total_amount is the sum of all the release amounts, in our case 1000 MOKs.

resource_cap represents the signer capability of the escrow or resource account (more explanation later on)

released_amount is present to account for the amount already withdrawn by Bob

Abilities in Move defines the boundary of a data struct. In our case, struct VestingSchedule has the abilities store and key. So, it can be stored in an account.

    //Map to store seed and corresponding resource account address
struct VestingCap has key {
vestingMap: SimpleMap< vector<u8>,address>,
}

This struct is to save the seed and corresponding resource address. Simple map is used here for efficiency which was previously imported from aptos_std module.

Following are the manually written errors which are self-explanatory:

    //errors
const ENO_INSUFFICIENT_FUND:u64=0;
const ENO_NO_VESTING:u64=1;
const ENO_SENDER_MISMATCH:u64=2;
const ENO_RECEIVER_MISMATCH:u64=3;
const ENO_WRONG_SENDER:u64=4;
const ENO_WRONG_RECEIVER:u64=5;

Each error is defined with a u64 number for ease in the identification of the error while executing a transaction.

Create Vesting function

In Aptos, different identifiers are used to define a function as per the access granted to a function. As our function needs to be called by user Ales, it is defined as the entry function. In Aptos move, the transaction signer comes as the input of the entry function as &signer which in our case is Alice. As Move requires the struct used to be already defined, we acquire the VestingCap. CoinType in our case is the Mokshya coin.

    public entry fun create_vesting<CoinType>(
account: &signer,
receiver: address,
release_amounts:vector<u64>,
release_times:vector<u64>,
total_amount:u64,
seeds: vector<u8>
)acquires VestingCap {

}

Firstly, we need to generate a resource account that will act as the escrow i.e vesting. Alice's account and seeds are used to create a resource account. The !exists<VestingCap>(account_addr) command verifies whether the VestingCap struct already exists in Alice’s account or not, move_to moves the struct into Alice’s account if it doesn’t already exist in Alice’s account. borrow_global_mut brings the mutable reference of the struct inside the Alice’s account and the seed and corresponding vesting address is added to the simple map for future access.

        let account_addr = signer::address_of(account);
let (vesting, vesting_cap) = account::create_resource_account(account, seeds); //resource account
let vesting_address = signer::address_of(&vesting);
if (!exists<VestingCap>(account_addr)) {
move_to(account, VestingCap { vestingMap: simple_map::create() })
};
let maps = borrow_global_mut<VestingCap>(account_addr);
simple_map::add(&mut maps.vestingMap, seeds,vesting_address);

The vesting_signer_from_cap is the signer capability of the resource account- vesting. It’s the signer form for the escrow vesting.

let vesting_signer_from_cap = account::create_signer_with_capability(&vesting_cap);

Below is a simple line of code that utilizes the vector module previously imported. We verify the length of release_amount,release_times and whether the release_amounts is equal to the sum of total amount or not

let length_of_schedule =  vector::length(&release_amounts);
let length_of_times = vector::length(&release_times);
assert!(length_of_schedule==length_of_times,ENO_INSUFFICIENT_FUND);
let i=0;
let total_amount_required=0;
while ( i < length_of_schedule )
{
let tmp = *vector::borrow(&release_amounts,i);
total_amount_required=total_amount_required+tmp;
i=i+1;
};
assert!(total_amount_required==total_amount,ENO_INSUFFICIENT_FUND);

As explained earlier, we derive the coin_address through a helper function, and all the information is saved in the resource account- vesting.

let released_amount=0;
let coin_address = coin_address<CoinType>();
move_to(&vesting_signer_from_cap, VestingSchedule{
sender:account_addr,
receiver,
coin_type:coin_address,
release_times,
release_amounts,
total_amount,
resource_cap:vesting_cap,
released_amount,
});

The helper function for deriving coin_address is defined below.

 /// A helper function that returns the address of CoinType.
fun coin_address<CoinType>(): address {
let type_info = type_info::type_of<CoinType>();
type_info::account_address(&type_info)
}

fun name(inputs): datatype {

expr1;

expr2

}
In function definion datatype after colon is the return type. A returning data can be left without a semi-colon inside the function (expr2 is the return type).

Now the only thing left is to transfer the coin from Alice to the vesting escrow. Firstly, we need to register the coin in the resource account. This step makes it necessary for an account to receiver only the coins the account desires. In the next step, the MOK coin is transferred to the vesting resource account

    managed_coin::register<CoinType>(&vesting_signer_from_cap); 
coin::transfer<CoinType>(account, vesting_address, total_amount);

Now, as your one function is ready you can compile your module.

module token_vesting::vesting {
............
............
............
}

Firstly, let’s create an account and designate dev-net as our cluster

aptos init 

Aptos CLI is now set up for account 20634774e3d40bf68fa86101723f2bc36c7b57bc5220e401475f2f1b27377a10 as profile default! Run `aptos — help` for more information about commands
{
“Result”: “Success”
}

Now, you can use the address obtained to compile your code

aptos move compile --named-addresses token_vesting="0xaddress_obtained_in_above_command"

Release Fund Function

This function is called by Bob to get his vested fund. As the function needs information on both the structs they need to be acquired at the definition of the function.

public entry fun release_fund<CoinType>(
receiver: &signer,
sender: address,
seeds: vector<u8>
)acquires VestingSchedule,VestingCap{

In the following lines, we borrow the VestingSchedule from the vesting resource account and its signer-capability to release the funds. Similarly, the sender and receiver are verified.

let receiver_addr = signer::address_of(receiver);
assert!(exists<VestingCap>(sender), ENO_NO_VESTING);
let maps = borrow_global<VestingCap>(sender);
let vesting_address = *simple_map::borrow(&maps.vestingMap, &seeds);
assert!(exists<VestingSchedule>(vesting_address), ENO_NO_VESTING);
let vesting_data = borrow_global_mut<VestingSchedule>(vesting_address);
let vesting_signer_from_cap = account::create_signer_with_capability(&vesting_data.resource_cap);
assert!(vesting_data.sender==sender,ENO_SENDER_MISMATCH);
assert!(vesting_data.receiver==receiver_addr,ENO_RECEIVER_MISMATCH);

Here, now or current time-stamp is derived from aptos-framework. Now, the amount of funds that the receiver can receive up to this time is calculated. So, if the date is Feb 12, the amount to be released should be sum of all the amounts i.e. 900 MOKS

let length_of_schedule =  vector::length(&vesting_data.release_amounts);
let i=0;
let amount_to_be_released=0;
let now = aptos_framework::timestamp::now_seconds();
while (i < length_of_schedule)
{
let tmp_amount = *vector::borrow(&vesting_data.release_amounts,i);
let tmp_time = *vector::borrow(&vesting_data.release_times,i);
if (tmp_time<=now)
{
amount_to_be_released=amount_to_be_released+tmp_amount;
};
i=i+1;
};
amount_to_be_released=amount_to_be_released-vesting_data.released_amount;

But, if, Bob has already in say Jan 30, the amount released at the time 500 MOKS must be deducted. So, the amount to be released will be 400.

Now, as already explained the MOKS coin is registered in Bob’s account and then transferred from the vesting - resource account to the Bob’s address.

 if (!coin::is_account_registered<CoinType>(receiver_addr))
{
managed_coin::register<CoinType>(receiver);
};
coin::transfer<CoinType>(&vesting_signer_from_cap,receiver_addr,amount_to_be_released);
vesting_data.released_amount=vesting_data.released_amount+amount_to_be_released;

Now, amount_to_be_released is added to the released_amount. So, the next time he will only be allowed access to the remaining funds.

Publishing Module

Now, we are ready to publish the module. Use the following command in terminal

aptos move publish --named-addresses token_vesting="0xaddress_obtained_in_above_command"

Interacting with Module

You can find the test code inside the tests folder in the repo.

creating vesting

//alice is the account 1 and bob is account 2
await faucetClient.fundAccount(account1.address(), 1000000000);//Airdropping
//Time and Amounts
const now = Math.floor(Date.now() / 1000)
//Any discrete amount and corresponding time
//can be provided to get variety of payment schedules
const release_amount =[10000, 50000, 10000, 30000];
const release_time_increment =[ 3, 20, 30];
var release_time:BigInt[]=[BigInt(now)]
release_time_increment.forEach((item) => {
let val=BigInt(now+item);
release_time.push(val);
});
const create_vesting_payloads = {
type: "entry_function_payload",
function: pid+"::vesting::create_vesting",
type_arguments: ["0x1::aptos_coin::AptosCoin"],
arguments: [account2.address(),release_amount,release_time,100000,"xyz"],
};
let txnRequest = await client.generateTransaction(account1.address(), create_vesting_payloads);
let bcsTxn = AptosClient.generateBCSTransaction(account1, txnRequest);
await client.submitSignedBCSTransaction(bcsTxn);

Release funds


await faucetClient.fundAccount(account2.address(), 1000000000);//Airdropping
//the receiver gets allocated fund as required
const create_getfunds_payloads = {
type: "entry_function_payload",
function: pid+"::vesting::release_fund",
type_arguments: ["0x1::aptos_coin::AptosCoin"],
arguments: [account1.address(),"xyz"],
};
let txnRequest = await client.generateTransaction(account2.address(), create_getfunds_payloads);
let bcsTxn = AptosClient.generateBCSTransaction(account2, txnRequest);
await client.submitSignedBCSTransaction(bcsTxn);

Conclusion

It’s just the first step in your journey at writing move modules in Aptos. You are welcome to contribute to the open-source solutions at Mokshya Protocol to sail in the journey of Smart Contract Development at Aptos Blockchain.

--

--